src/Controller/SellerController.php line 2163

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Shop;
  4. use App\Entity\Product;
  5. use App\Entity\Category;
  6. use App\Entity\ShopPlan;
  7. use App\Entity\OrderItem;
  8. use Symfony\Bridge\Doctrine\Attribute\MapEntity;
  9. use App\Entity\User;
  10. use App\Form\SellerShopType;
  11. use App\Repository\ShopPlanRepository;
  12. use App\Service\NotificationService;
  13. use App\Service\ShopLimitService;
  14. use Doctrine\ORM\EntityManagerInterface;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\HttpFoundation\JsonResponse;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\Mailer\MailerInterface;
  20. use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  21. use Symfony\Component\Mime\Address as EmailAddress;
  22. use Symfony\Component\Routing\Annotation\Route;
  23. use Symfony\Component\String\Slugger\AsciiSlugger;
  24. use function Symfony\Component\String\s;
  25. #[Route('/seller_space'name'seller_')]
  26. class SellerController extends AbstractController
  27. {
  28.     #[Route('/'name'index')]
  29.     public function indexSeller(EntityManagerInterface $em): Response
  30.     {
  31.         if (!$this->getUser()) {
  32.             return $this->redirectToRoute('ui_app_login');
  33.         }
  34.         if (!$this->isAuthorized()) {
  35.             return $this->render('misc/access_denied.html.twig');
  36.         }
  37.         $user $this->getUser();
  38.         if (!$user instanceof User) {
  39.             return $this->redirectToRoute('ui_app_login');
  40.         }
  41.         // Récupérer toutes les boutiques de l'utilisateur (relation ManyToMany)
  42.         $shops $em->getRepository(Shop::class)
  43.             ->createQueryBuilder('s')
  44.             ->where(':user MEMBER OF s.manager')
  45.             ->setParameter('user'$user)
  46.             ->getQuery()
  47.             ->getResult();
  48.         $shopsCount count($shops);
  49.         // Initialiser les statistiques
  50.         $stats = [
  51.             'shopsCount' => $shopsCount,
  52.             'totalProducts' => 0,
  53.             'activeProducts' => 0,
  54.             'totalOrders' => 0,
  55.             'totalRevenue' => 0.0,
  56.             'averageCart' => 0.0,
  57.             'conversionRate' => 0.0,
  58.             'totalFollowers' => 0,
  59.             'totalViews' => 0,
  60.             'recentOrders' => [],
  61.             'topShops' => []
  62.         ];
  63.         if ($shopsCount 0) {
  64.             // Calculer le nombre total de produits
  65.             $totalProducts $em->getRepository(Product::class)
  66.                 ->createQueryBuilder('p')
  67.                 ->select('COUNT(p.id)')
  68.                 ->where('p.shop IN (:shops)')
  69.                 ->setParameter('shops'$shops)
  70.                 ->getQuery()
  71.                 ->getSingleScalarResult();
  72.             $stats['totalProducts'] = (int)$totalProducts;
  73.             // Calculer le nombre de produits actifs
  74.             $activeProducts $em->getRepository(Product::class)
  75.                 ->createQueryBuilder('p')
  76.                 ->select('COUNT(p.id)')
  77.                 ->where('p.shop IN (:shops)')
  78.                 ->andWhere('p.isActive = :active')
  79.                 ->setParameter('shops'$shops)
  80.                 ->setParameter('active'true)
  81.                 ->getQuery()
  82.                 ->getSingleScalarResult();
  83.             $stats['activeProducts'] = (int)$activeProducts;
  84.             // Calculer le nombre total de commandes
  85.             try {
  86.                 $totalOrders $em->createQueryBuilder()
  87.                     ->select('COUNT(DISTINCT o.id)')
  88.                     ->from(\App\Entity\Order::class, 'o')
  89.                     ->join('o.items''oi')
  90.                     ->join('oi.product''p')
  91.                     ->where('p.shop IN (:shops)')
  92.                     ->setParameter('shops'$shops)
  93.                     ->getQuery()
  94.                     ->getSingleScalarResult();
  95.                 $stats['totalOrders'] = (int)$totalOrders;
  96.                 // Calculer le chiffre d'affaires total
  97.                 $totalRevenue $em->createQueryBuilder()
  98.                     ->select('COALESCE(SUM(CAST(oi.totalPrice AS DECIMAL(10,2))), 0)')
  99.                     ->from(OrderItem::class, 'oi')
  100.                     ->join('oi.product''p')
  101.                     ->join('oi.order''o')
  102.                     ->where('p.shop IN (:shops)')
  103.                     ->andWhere('o.paymentStatus = :paid')
  104.                     ->setParameter('shops'$shops)
  105.                     ->setParameter('paid''paid')
  106.                     ->getQuery()
  107.                     ->getSingleScalarResult();
  108.                 $stats['totalRevenue'] = (float)$totalRevenue;
  109.                 // Calculer le panier moyen
  110.                 if ($stats['totalOrders'] > 0) {
  111.                     $stats['averageCart'] = $stats['totalRevenue'] / $stats['totalOrders'];
  112.                 }
  113.                 // Récupérer les commandes récentes (10 dernières)
  114.                 $recentOrders $em->getRepository(\App\Entity\Order::class)
  115.                     ->createQueryBuilder('o')
  116.                     ->select('DISTINCT o')
  117.                     ->leftJoin('o.items''oi')
  118.                     ->leftJoin('oi.product''p')
  119.                     ->leftJoin('o.customer''c')
  120.                     ->addSelect('oi''p''c')
  121.                     ->where('p.shop IN (:shops)')
  122.                     ->setParameter('shops'$shops)
  123.                     ->orderBy('o.orderedAt''DESC')
  124.                     ->setMaxResults(10)
  125.                     ->getQuery()
  126.                     ->getResult();
  127.                 $stats['recentOrders'] = $recentOrders;
  128.             } catch (\Exception $e) {
  129.                 // Si l'entité Order n'existe pas encore, on continue avec des valeurs par défaut
  130.             }
  131.             // Calculer le total des followers (somme des followers de toutes les boutiques)
  132.             foreach ($shops as $shop) {
  133.                 $stats['totalFollowers'] += $shop->getFollowerCount() ?? 0;
  134.                 $stats['totalViews'] += $shop->getViewCount() ?? 0;
  135.             }
  136.             // Calculer le taux de conversion (approximation basée sur les vues et commandes)
  137.             if ($stats['totalViews'] > 0) {
  138.                 $stats['conversionRate'] = ($stats['totalOrders'] / $stats['totalViews']) * 100;
  139.             }
  140.             // Calculer les top boutiques par revenus
  141.             $topShops = [];
  142.             foreach ($shops as $shop) {
  143.                 try {
  144.                     $shopRevenue $em->createQueryBuilder()
  145.                         ->select('COALESCE(SUM(CAST(oi.totalPrice AS DECIMAL(10,2))), 0)')
  146.                         ->from(OrderItem::class, 'oi')
  147.                         ->join('oi.product''p')
  148.                         ->join('oi.order''o')
  149.                         ->where('p.shop = :shop')
  150.                         ->andWhere('o.paymentStatus = :paid')
  151.                         ->setParameter('shop'$shop)
  152.                         ->setParameter('paid''paid')
  153.                         ->getQuery()
  154.                         ->getSingleScalarResult();
  155.                     
  156.                     $topShops[] = [
  157.                         'shop' => $shop,
  158.                         'revenue' => (float)$shopRevenue
  159.                     ];
  160.                 } catch (\Exception $e) {
  161.                     $topShops[] = [
  162.                         'shop' => $shop,
  163.                         'revenue' => 0.0
  164.                     ];
  165.                 }
  166.             }
  167.             // Trier par revenus décroissants
  168.             usort($topShops, function($a$b) {
  169.                 return $b['revenue'] <=> $a['revenue'];
  170.             });
  171.             $stats['topShops'] = array_slice($topShops05); // Top 5
  172.         }
  173.         return $this->render('seller/index.html.twig', [
  174.             'current_menu' => 'home',
  175.             'stats' => $stats,
  176.             'shops' => $shops
  177.         ]);
  178.     }
  179.     #[Route('/help/how-to-sell'name'help_how_to_sell')]
  180.     public function howToSell(): Response
  181.     {
  182.         return $this->render('seller/help/how_to_sell.html.twig', [
  183.             'current_menu' => 'help'
  184.         ]);
  185.     }
  186.     #[Route('/help/pricing'name'help_pricing')]
  187.     public function pricing(EntityManagerInterface $em): Response
  188.     {
  189.         $shopPlans $em->getRepository(ShopPlan::class)->findAll();
  190.         return $this->render('seller/help/pricing.html.twig', [
  191.             'current_menu' => 'help',
  192.             'shopPlans' => $shopPlans
  193.         ]);
  194.     }
  195.     #[Route('/help/support'name'help_support')]
  196.     public function support(): Response
  197.     {
  198.         return $this->render('seller/help/support.html.twig', [
  199.             'current_menu' => 'help'
  200.         ]);
  201.     }
  202.     #[Route('/shop/new'name'shop_new')]
  203.     public function newShopSeller(Request $requestEntityManagerInterface $entityManagerNotificationService $notificationServiceMailerInterface $mailerShopLimitService $shopLimitService): Response
  204.     {
  205.         if (!$this->getUser()) {
  206.             return $this->redirectToRoute('ui_app_login');
  207.         }
  208.         if (!$this->isAuthorized()) {
  209.             return $this->render('misc/access_denied.html.twig');
  210.         }
  211.         
  212.         // Récupérer l'utilisateur et vérifier la limite dès l'accès à la page
  213.         $authenticatedUser $this->getUser();
  214.         if (!$authenticatedUser instanceof User) {
  215.             return $this->redirectToRoute('ui_app_login');
  216.         }
  217.         $userId $authenticatedUser->getId();
  218.         $user $entityManager->getRepository(User::class)->find($userId);
  219.         if (!$user) {
  220.             throw $this->createNotFoundException('Utilisateur introuvable');
  221.         }
  222.         // Vérifier la limite de boutiques par utilisateur AVANT d'afficher le formulaire
  223.         $shopLimitCheck $shopLimitService->canUserCreateShop($user);
  224.         if (!$shopLimitCheck['allowed']) {
  225.             // Afficher un message d'erreur visible au lieu de rediriger
  226.             $this->addFlash('error'$shopLimitCheck['message']);
  227.             
  228.             // Récupérer tous les plans pour le template (même si la limite est atteinte)
  229.             $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  230.             $plansData = [];
  231.             foreach ($shopPlans as $plan) {
  232.                 $plansData[$plan->getId()] = [
  233.                     'id' => $plan->getId(),
  234.                     'name' => $plan->getName(),
  235.                     'requireSiretNif' => $plan->isRequireSiretNif(),
  236.                     'requireIban' => $plan->isRequireIban(),
  237.                     'maxProducts' => $plan->getMaxProducts(),
  238.                     'maxEmployees' => $plan->getMaxEmployees(),
  239.                 ];
  240.             }
  241.             
  242.             $shop = new Shop();
  243.             $form $this->createForm(SellerShopType::class, $shop);
  244.             
  245.             return $this->render('seller/new_shop_simple.html.twig', [
  246.                 'current_menu' => 'shop',
  247.                 'current' => 'shopNew',
  248.                 'shop' => $shop,
  249.                 'form' => $form->createView(),
  250.                 'plansData' => $plansData,
  251.                 'shopLimitCheck' => $shopLimitCheck // Passer les infos de limite au template
  252.             ]);
  253.         }
  254.         
  255.         $shop = new Shop();
  256.         $form $this->createForm(SellerShopType::class, $shop);
  257.         $form->handleRequest($request);
  258.         if ($form->isSubmitted() && $form->isValid()) {
  259.             // Vérifier à nouveau la limite lors de la soumission (au cas où elle aurait changé)
  260.             $shopLimitCheck $shopLimitService->canUserCreateShop($user);
  261.             if (!$shopLimitCheck['allowed']) {
  262.                 $this->addFlash('error'$shopLimitCheck['message']);
  263.                 
  264.                 // Récupérer tous les plans pour les contraintes JavaScript
  265.                 $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  266.                 $plansData = [];
  267.                 foreach ($shopPlans as $plan) {
  268.                     $plansData[$plan->getId()] = [
  269.                         'id' => $plan->getId(),
  270.                         'name' => $plan->getName(),
  271.                         'requireSiretNif' => $plan->isRequireSiretNif(),
  272.                         'requireIban' => $plan->isRequireIban(),
  273.                         'maxProducts' => $plan->getMaxProducts(),
  274.                         'maxEmployees' => $plan->getMaxEmployees(),
  275.                     ];
  276.                 }
  277.                 
  278.                 return $this->render('seller/new_shop_simple.html.twig', [
  279.                     'current_menu' => 'shop',
  280.                     'current' => 'shopNew',
  281.                     'shop' => $shop,
  282.                     'form' => $form->createView(),
  283.                     'plansData' => $plansData,
  284.                     'shopLimitCheck' => $shopLimitCheck
  285.                 ]);
  286.             }
  287.             // Configuration de base
  288.             $shop->setIsActive(true);
  289.             $shop->setCreatedAt(new \DateTimeImmutable('now'));
  290.             $shop->setCurrentEmployees(1);
  291.             
  292.             // Génération automatique du slug si nécessaire
  293.             if ($shop->getName()) {
  294.                 $slugger = new AsciiSlugger();
  295.                 $shop->setSlug(strtolower($slugger->slug($shop->getName())->toString()));
  296.             }
  297.             
  298.             // Initialisation des compteurs
  299.             $shop->setCurrentOrders(0);
  300.             $shop->setCurrentProducts(0);
  301.             $shop->setCurrentCaLimit(0);
  302.             $shop->setCurrentRevenue('0.000');
  303.             $shop->setCurrentStorage(0);
  304.             
  305.             // Initialisation des statistiques
  306.             $shop->setViewCount(0);
  307.             $shop->setFollowerCount(0);
  308.             $shop->setAverageRating(0.0);
  309.             $shop->setReviewCount(0);
  310.             
  311.             // Configuration des statuts par défaut
  312.             if (!$shop->getStatus()) {
  313.                 $shop->setStatus('pending');
  314.             }
  315.             if (!$shop->getVerificationStatus()) {
  316.                 $shop->setVerificationStatus('unverified');
  317.             }
  318.             
  319.             // Configuration des options par défaut (uniquement pour les champs modifiables par le vendeur)
  320.             if ($shop->isIsActive() === null) {
  321.                 $shop->setIsActive(true);
  322.             }
  323.             if ($shop->isAllowReviews() === null) {
  324.                 $shop->setAllowReviews(true);
  325.             }
  326.             if ($shop->isAllowMessages() === null) {
  327.                 $shop->setAllowMessages(true);
  328.             }
  329.             if ($shop->isShowContactInfo() === null) {
  330.                 $shop->setShowContactInfo(true);
  331.             }
  332.             if ($shop->isShowSocialLinks() === null) {
  333.                 $shop->setShowSocialLinks(true);
  334.             }
  335.             
  336.             // Champs gérés uniquement par l'administrateur (ne pas modifier)
  337.             // isVerified, isPremium, status, verificationStatus sont gérés par l'admin
  338.             if (!$shop->getStatus()) {
  339.                 $shop->setStatus('pending');
  340.             }
  341.             if (!$shop->getVerificationStatus()) {
  342.                 $shop->setVerificationStatus('unverified');
  343.             }
  344.             if ($shop->isIsVerified() === null) {
  345.                 $shop->setIsVerified(false);
  346.             }
  347.             if ($shop->isIsPremium() === null) {
  348.                 $shop->setIsPremium(false);
  349.             }
  350.             
  351.             // Gestion des fichiers uploadés
  352.             $logoFile $form->get('logo')->getData();
  353.             if ($logoFile && $logoFile->isValid()) {
  354.                 $maxSize 15 1024 1024// 15MB
  355.                 if ($logoFile->getSize() > $maxSize) {
  356.                     $this->addFlash('error''Le logo est trop volumineux. Taille maximale : 15MB.');
  357.                 } else {
  358.                     $newFilename uniqid().'.'.$logoFile->guessExtension();
  359.                     $logoFile->move(
  360.                         $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  361.                         $newFilename
  362.                     );
  363.                     $shop->setLogo('uploads/shops/'.$newFilename);
  364.                 }
  365.             }
  366.             
  367.             // Gestion des images de bannière (jusqu'à 7 images, 15MB max chacune)
  368.             $allBannerImages $shop->getAllBannerImages();
  369.             
  370.             // Gestion des nouvelles images multiples de bannière
  371.             $bannerImagesFile $form->get('bannerImages')->getData();
  372.             if ($bannerImagesFile) {
  373.                 $maxSize 15 1024 1024// 15MB
  374.                 foreach ($bannerImagesFile as $file) {
  375.                     if ($file && $file->isValid()) {
  376.                         // Vérifier la taille du fichier
  377.                         if ($file->getSize() > $maxSize) {
  378.                             $this->addFlash('error''Le fichier "' $file->getClientOriginalName() . '" est trop volumineux. Taille maximale : 15MB.');
  379.                             continue;
  380.                         }
  381.                         
  382.                         $newFilename uniqid().'.'.$file->guessExtension();
  383.                         $file->move(
  384.                             $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  385.                             $newFilename
  386.                         );
  387.                         $allBannerImages[] = 'uploads/shops/'.$newFilename;
  388.                     }
  389.                 }
  390.             }
  391.             // Dédupliquer et limiter à 7 images max
  392.             $allBannerImages array_values(array_unique($allBannerImages));
  393.             if (count($allBannerImages) > 7) {
  394.                 $this->addFlash('error''Maximum 7 images de bannière autorisées. Les images supplémentaires ont été ignorées.');
  395.                 $allBannerImages array_slice($allBannerImages07);
  396.             }
  397.             $shop->setBannerImages($allBannerImages);
  398.             
  399.             // Contraintes basées sur le plan sélectionné
  400.             if ($shop->getPlan()) {
  401.                 $plan $shop->getPlan();
  402.                 $errors = [];
  403.                 
  404.                 // Vérifier les champs requis par le plan
  405.                 if ($plan->isRequireSiretNif() && !$shop->getSiretNif()) {
  406.                     $errors[] = 'Votre plan exige un SIRET/NIF. Veuillez le renseigner.';
  407.                 }
  408.                 
  409.                 if ($plan->isRequireIban() && !$shop->getIban()) {
  410.                     $errors[] = 'Votre plan exige un IBAN. Veuillez le renseigner.';
  411.                 }
  412.                 
  413.                 // Si des erreurs, retourner au formulaire
  414.                 if (!empty($errors)) {
  415.                     foreach ($errors as $error) {
  416.                         $this->addFlash('error'$error);
  417.                     }
  418.                     return $this->render('seller/new_shop_simple.html.twig', [
  419.                         'current_menu' => 'shop',
  420.                         'current' => 'shopNew',
  421.                         'shop' => $shop,
  422.                         'form' => $form->createView()
  423.                     ]);
  424.                 }
  425.             }
  426.             
  427.             $shop->addManager($user);
  428.             $entityManager->persist($shop);
  429.             $entityManager->flush();
  430.             // Créer une notification de félicitations
  431.             $notificationService->createShopCreatedNotification($user$shop);
  432.             // Envoyer un email de félicitations
  433.             try {
  434.                 $email = (new TemplatedEmail())
  435.                     ->from(new EmailAddress('no-reply@maketou-ht.com''MaketOu'))
  436.                     ->to(new EmailAddress($user->getEmail(), $user->getFirstname() . ' ' $user->getLastname()))
  437.                     ->subject('Félicitations ! Votre boutique "' $shop->getName() . '" a été créée')
  438.                     ->htmlTemplate('emails/shop_created.html.twig')
  439.                     ->context([
  440.                         'user' => $user,
  441.                         'shop' => $shop,
  442.                     ]);
  443.                 $mailer->send($email);
  444.             } catch (\Exception $e) {
  445.                 // Log l'erreur mais ne bloque pas la création de la boutique
  446.                 // On peut logger l'erreur ici si nécessaire
  447.             }
  448.             $this->addFlash('success''Boutique créée avec succès ! Vous avez reçu un email de confirmation.');
  449.             return $this->redirectToRoute('seller_index', [], Response::HTTP_SEE_OTHER);
  450.         }
  451.         // Récupérer tous les plans pour les contraintes JavaScript
  452.         $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  453.         $plansData = [];
  454.         foreach ($shopPlans as $plan) {
  455.             $plansData[$plan->getId()] = [
  456.                 'id' => $plan->getId(),
  457.                 'name' => $plan->getName(),
  458.                 'requireSiretNif' => $plan->isRequireSiretNif(),
  459.                 'requireIban' => $plan->isRequireIban(),
  460.                 'maxProducts' => $plan->getMaxProducts(),
  461.                 'maxEmployees' => $plan->getMaxEmployees(),
  462.             ];
  463.         }
  464.         
  465.         return $this->render('seller/new_shop_simple.html.twig', [
  466.             'current_menu' => 'shop',
  467.             'current' => 'shopNew',
  468.             'shop' => $shop,
  469.             'form' => $form->createView(),
  470.             'plansData' => $plansData,
  471.             'shopLimitCheck' => $shopLimitCheck // Passer les infos de limite au template
  472.         ]);
  473.     }
  474.     #[Route('/shop/{slug}/show'name'shop_show')]
  475.     public function showShopSeller(string $slugRequest $requestEntityManagerInterface $em): Response
  476.     {
  477.         if (!$this->getUser()) {
  478.             return $this->redirectToRoute('ui_app_login');
  479.         }
  480.         if (!$this->isAuthorized()) {
  481.             return $this->render('misc/access_denied.html.twig');
  482.         }
  483.         // Récupérer la boutique
  484.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  485.         
  486.         if (!$shop) {
  487.             $this->addFlash('error''Boutique non trouvée. Le slug "' $slug '" ne correspond à aucune boutique.');
  488.             return $this->redirectToRoute('seller_index');
  489.         }
  490.         // Vérifier que la boutique appartient à l'utilisateur connecté
  491.         $canEdit $shop->getManager()->contains($this->getUser());
  492.         // Calculer les statistiques dynamiques
  493.         $productsCount $shop->getActiveProductsCount();
  494.         // Optimisation: Utiliser une requête COUNT au lieu de charger la collection
  495.         $totalProducts $em->getRepository(Product::class)
  496.             ->createQueryBuilder('p')
  497.             ->select('COUNT(p.id)')
  498.             ->where('p.shop = :shop')
  499.             ->setParameter('shop'$shop)
  500.             ->getQuery()
  501.             ->getSingleScalarResult();
  502.         
  503.         // Optimisation: Utiliser des requêtes séparées optimisées au lieu de charger toutes les commandes
  504.         $recentOrders = [];
  505.         $ordersCount 0;
  506.         $revenue 0.0;
  507.         
  508.         try {
  509.             $orderRepo $em->getRepository(\App\Entity\Order::class);
  510.             
  511.             // Compter les commandes avec une requête COUNT optimisée
  512.             $ordersCount $orderRepo->createQueryBuilder('o')
  513.                 ->select('COUNT(DISTINCT o.id)')
  514.                 ->join('o.items''oi')
  515.                 ->join('oi.product''p')
  516.                 ->where('p.shop = :shop')
  517.                 ->setParameter('shop'$shop)
  518.                 ->getQuery()
  519.                 ->getSingleScalarResult();
  520.             
  521.             // Calculer le chiffre d'affaires directement avec SUM (évite de charger toutes les commandes)
  522.             $revenueResult $em->createQueryBuilder()
  523.                 ->select('COALESCE(SUM(CAST(oi.totalPrice AS DECIMAL(10,2))), 0)')
  524.                 ->from(\App\Entity\OrderItem::class, 'oi')
  525.                 ->join('oi.product''p')
  526.                 ->join('oi.order''o')
  527.                 ->where('p.shop = :shop')
  528.                 ->andWhere('o.paymentStatus = :paid')
  529.                 ->setParameter('shop'$shop)
  530.                 ->setParameter('paid''paid')
  531.                 ->getQuery()
  532.                 ->getSingleScalarResult();
  533.             
  534.             $revenue = (float)$revenueResult;
  535.             
  536.             // Récupérer seulement les 10 commandes récentes avec eager loading
  537.             $recentOrders $orderRepo->createQueryBuilder('o')
  538.                 ->select('DISTINCT o')
  539.                 ->leftJoin('o.items''oi')
  540.                 ->leftJoin('oi.product''p')
  541.                 ->leftJoin('o.customer''c')
  542.                 ->addSelect('oi''p''c'// Eager loading pour éviter N+1
  543.                 ->where('p.shop = :shop')
  544.                 ->setParameter('shop'$shop)
  545.                 ->orderBy('o.orderedAt''DESC')
  546.                 ->setMaxResults(10)
  547.                 ->getQuery()
  548.                 ->getResult();
  549.         } catch (\Exception $e) {
  550.             // Si l'entité Order n'existe pas encore ou erreur, on continue sans
  551.             $ordersCount 0;
  552.             $revenue 0.0;
  553.             $recentOrders = [];
  554.         }
  555.         
  556.         // Calculer le solde disponible (revenue - dépenses potentielles)
  557.         // Pour l'instant, on utilise le revenue comme solde disponible
  558.         $availableBalance $revenue;
  559.         
  560.         // Calculer la date de dernière mise à jour (date de création ou dernière commande)
  561.         $lastUpdate $shop->getCreatedAt();
  562.         if ($ordersCount && !empty($recentOrders)) {
  563.             // Utiliser la date de la commande la plus récente
  564.             $lastUpdate $recentOrders[0]->getOrderedAt();
  565.         }
  566.         
  567.         // Calculer le temps depuis la dernière mise à jour
  568.         $updateText 'Aujourd\'hui';
  569.         if ($lastUpdate) {
  570.             $now = new \DateTimeImmutable();
  571.             $diff $now->diff($lastUpdate);
  572.             if ($diff->days 0) {
  573.                 if ($diff->days == 1) {
  574.                     $updateText 'Hier';
  575.                 } elseif ($diff->days 7) {
  576.                     $updateText 'Il y a ' $diff->days ' jours';
  577.                 } elseif ($diff->days 30) {
  578.                     $weeks floor($diff->days 7);
  579.                     $updateText 'Il y a ' $weeks ' semaine' . ($weeks 's' '');
  580.                 } else {
  581.                     $months floor($diff->days 30);
  582.                     $updateText 'Il y a ' $months ' mois';
  583.                 }
  584.             }
  585.         }
  586.         // Calculer les statistiques de vues par période (derniers 30 jours)
  587.         $viewsLast30Days 0;
  588.         try {
  589.             // Pour l'instant, on utilise viewCount comme approximation
  590.             $viewsLast30Days = (int)($shop->getViewCount() * 0.3); // Approximation
  591.         } catch (\Exception $e) {
  592.             // Ignorer si erreur
  593.         }
  594.         // Récupérer les produits populaires (actifs uniquement, triés par ventes puis vues)
  595.         $popularProducts = [];
  596.         try {
  597.             $productRepo $em->getRepository(\App\Entity\Product::class);
  598.             $popularProducts $productRepo->createQueryBuilder('p')
  599.                 ->where('p.shop = :shop')
  600.                 ->andWhere('p.isActive = :isActive')
  601.                 ->setParameter('shop'$shop)
  602.                 ->setParameter('isActive'true)
  603.                 ->orderBy('p.salesCount''DESC')
  604.                 ->addOrderBy('p.viewCount''DESC')
  605.                 ->addOrderBy('p.publishedAt''DESC')
  606.                 ->setMaxResults(6)
  607.                 ->getQuery()
  608.                 ->getResult();
  609.         } catch (\Exception $e) {
  610.             // En cas d'erreur, utiliser les produits actifs de la boutique
  611.             $popularProducts $shop->getProducts()->filter(function($product) {
  612.                 return $product->isIsActive();
  613.             })->toArray();
  614.             // Trier par ventes puis vues
  615.             usort($popularProducts, function($a$b) {
  616.                 $salesDiff = ($b->getSalesCount() ?? 0) - ($a->getSalesCount() ?? 0);
  617.                 if ($salesDiff !== 0) return $salesDiff;
  618.                 return ($b->getViewCount() ?? 0) - ($a->getViewCount() ?? 0);
  619.             });
  620.             $popularProducts array_slice($popularProducts06);
  621.         }
  622.         return $this->render('seller/show_shop.html.twig', [
  623.             'current_menu' => 'shop',
  624.             'slugShop' => $shop->getSlug(),
  625.             'shop' => $shop,
  626.             'canEdit' => $canEdit,
  627.             'recentOrders' => $recentOrders,
  628.             'viewsLast30Days' => $viewsLast30Days,
  629.             'popularProducts' => $popularProducts,
  630.             'stats' => [
  631.                 'productsCount' => $productsCount,
  632.                 'totalProducts' => $totalProducts,
  633.                 'ordersCount' => $ordersCount,
  634.                 'revenue' => $revenue,
  635.                 'availableBalance' => $availableBalance,
  636.                 'lastUpdate' => $updateText
  637.             ]
  638.         ]);
  639.     }
  640.     #[Route('/shop/{slug}/edit'name'shop_edit')]
  641.     public function editShopSeller(string $slugRequest $requestEntityManagerInterface $entityManager): Response
  642.     {
  643.         if (!$this->getUser()) {
  644.             return $this->redirectToRoute('ui_app_login');
  645.         }
  646.         if (!$this->isAuthorized()) {
  647.             return $this->render('misc/access_denied.html.twig');
  648.         }
  649.         $shop $entityManager->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  650.         
  651.         if (!$shop) {
  652.             $this->addFlash('error''Boutique non trouvée.');
  653.             return $this->redirectToRoute('seller_index');
  654.         }
  655.         // Vérifier que la boutique appartient à l'utilisateur connecté
  656.         if (!$shop->getManager()->contains($this->getUser())) {
  657.             $this->addFlash('error''Vous n\'avez pas la permission de modifier cette boutique.');
  658.             return $this->redirectToRoute('seller_shop_show', ['slug' => $shop->getSlug()]);
  659.         }
  660.         $form $this->createForm(SellerShopType::class, $shop);
  661.         $form->handleRequest($request);
  662.         // Gérer les uploads AJAX partiels (images de bannière uniquement)
  663.         $isAjax $request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest';
  664.         $uploadedBannerImages $request->files->all('bannerImages');
  665.         $isPartialUpload $isAjax && !empty($uploadedBannerImages);
  666.         if ($isPartialUpload) {
  667.             // Upload AJAX partiel pour les images de bannière
  668.             $allBannerImages $shop->getAllBannerImages();
  669.             $maxSize 15 1024 1024// 15MB
  670.             
  671.             foreach ($uploadedBannerImages as $file) {
  672.                 if ($file && $file->isValid()) {
  673.                     if ($file->getSize() > $maxSize) {
  674.                         return new JsonResponse([
  675.                             'success' => false,
  676.                             'message' => 'Le fichier "' $file->getClientOriginalName() . '" est trop volumineux. Taille maximale : 15MB.'
  677.                         ], 400);
  678.                     }
  679.                     
  680.                     if (count($allBannerImages) >= 7) {
  681.                         return new JsonResponse([
  682.                             'success' => false,
  683.                             'message' => 'Maximum 7 images de bannière autorisées.'
  684.                         ], 400);
  685.                     }
  686.                     
  687.                     $newFilename uniqid().'.'.$file->guessExtension();
  688.                     $file->move(
  689.                         $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  690.                         $newFilename
  691.                     );
  692.                     $allBannerImages[] = 'uploads/shops/'.$newFilename;
  693.                 }
  694.             }
  695.             
  696.             $allBannerImages array_values(array_unique($allBannerImages));
  697.             $shop->setBannerImages($allBannerImages);
  698.             $entityManager->flush();
  699.             
  700.             return new JsonResponse([
  701.                 'success' => true,
  702.                 'message' => count($uploadedBannerImages) . ' image(s) ajoutée(s) avec succès',
  703.                 'data' => [
  704.                     'bannerImages' => $shop->getAllBannerImages()
  705.                 ]
  706.             ]);
  707.         }
  708.         if ($form->isSubmitted() && $form->isValid()) {
  709.             // Génération automatique du slug si le nom a changé
  710.             if ($shop->getName()) {
  711.                 $slugger = new AsciiSlugger();
  712.                 $newSlug strtolower($slugger->slug($shop->getName())->toString());
  713.                 // Vérifier si le slug existe déjà pour une autre boutique
  714.                 $existingShop $entityManager->getRepository(Shop::class)->findOneBy(['slug' => $newSlug]);
  715.                 if (!$existingShop || $existingShop->getId() === $shop->getId()) {
  716.                     $shop->setSlug($newSlug);
  717.                 }
  718.             }
  719.             
  720.             // Gestion des fichiers uploadés
  721.             $logoFile $form->get('logo')->getData();
  722.             if ($logoFile && $logoFile->isValid()) {
  723.                 $maxSize 15 1024 1024// 15MB
  724.                 if ($logoFile->getSize() > $maxSize) {
  725.                     $this->addFlash('error''Le logo est trop volumineux. Taille maximale : 15MB.');
  726.                 } else {
  727.                     $newFilename uniqid().'.'.$logoFile->guessExtension();
  728.                     $logoFile->move(
  729.                         $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  730.                         $newFilename
  731.                     );
  732.                     $shop->setLogo('uploads/shops/'.$newFilename);
  733.                 }
  734.             }
  735.             
  736.             // Gestion des images de bannière (jusqu'à 7 images, 15MB max chacune)
  737.             $allBannerImages $shop->getAllBannerImages();
  738.             
  739.             // Gérer la suppression d'images existantes
  740.             $removeBannerImages $request->request->all('removeBannerImages');
  741.             if ($removeBannerImages) {
  742.                 foreach ($removeBannerImages as $imageToRemove) {
  743.                     $allBannerImages array_filter($allBannerImages, function($img) use ($imageToRemove) {
  744.                         return $img !== $imageToRemove;
  745.                     });
  746.                 }
  747.                 $allBannerImages array_values($allBannerImages);
  748.             }
  749.             
  750.             // Gestion des nouvelles images multiples de bannière
  751.             $bannerImagesFile $form->get('bannerImages')->getData();
  752.             if ($bannerImagesFile) {
  753.                 $maxSize 15 1024 1024// 15MB
  754.                 foreach ($bannerImagesFile as $file) {
  755.                     if ($file && $file->isValid()) {
  756.                         // Vérifier la taille du fichier
  757.                         if ($file->getSize() > $maxSize) {
  758.                             $this->addFlash('error''Le fichier "' $file->getClientOriginalName() . '" est trop volumineux. Taille maximale : 15MB.');
  759.                             continue;
  760.                         }
  761.                         
  762.                         $newFilename uniqid().'.'.$file->guessExtension();
  763.                         $file->move(
  764.                             $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  765.                             $newFilename
  766.                         );
  767.                         $allBannerImages[] = 'uploads/shops/'.$newFilename;
  768.                     }
  769.                 }
  770.             }
  771.             // Dédupliquer et limiter à 7 images max
  772.             $allBannerImages array_values(array_unique($allBannerImages));
  773.             if (count($allBannerImages) > 7) {
  774.                 $this->addFlash('error''Maximum 7 images de bannière autorisées. Les images supplémentaires ont été ignorées.');
  775.                 $allBannerImages array_slice($allBannerImages07);
  776.             }
  777.             $shop->setBannerImages($allBannerImages);
  778.             
  779.             // Contraintes basées sur le plan sélectionné
  780.             if ($shop->getPlan()) {
  781.                 $plan $shop->getPlan();
  782.                 $errors = [];
  783.                 
  784.                 // Vérifier les champs requis par le plan
  785.                 if ($plan->isRequireSiretNif() && !$shop->getSiretNif()) {
  786.                     $errors[] = 'Votre plan exige un SIRET/NIF. Veuillez le renseigner.';
  787.                 }
  788.                 
  789.                 if ($plan->isRequireIban() && !$shop->getIban()) {
  790.                     $errors[] = 'Votre plan exige un IBAN. Veuillez le renseigner.';
  791.                 }
  792.                 
  793.                 // Si des erreurs, retourner au formulaire
  794.                 if (!empty($errors)) {
  795.                     foreach ($errors as $error) {
  796.                         $this->addFlash('error'$error);
  797.                     }
  798.                     // Récupérer tous les plans pour les contraintes JavaScript
  799.                     $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  800.                     $plansData = [];
  801.                     foreach ($shopPlans as $planItem) {
  802.                         $plansData[$planItem->getId()] = [
  803.                             'id' => $planItem->getId(),
  804.                             'name' => $planItem->getName(),
  805.                             'requireSiretNif' => $planItem->isRequireSiretNif(),
  806.                             'requireIban' => $planItem->isRequireIban(),
  807.                             'maxProducts' => $planItem->getMaxProducts(),
  808.                             'maxEmployees' => $planItem->getMaxEmployees(),
  809.                         ];
  810.                     }
  811.                     return $this->render('seller/new_shop_simple.html.twig', [
  812.                         'current_menu' => 'shop',
  813.                         'current' => 'shopEdit',
  814.                         'shop' => $shop,
  815.                         'form' => $form->createView(),
  816.                         'plansData' => $plansData
  817.                     ]);
  818.                 }
  819.             }
  820.             
  821.             $entityManager->flush();
  822.             $this->addFlash('success''Boutique modifiée avec succès !');
  823.             return $this->redirectToRoute('seller_shop_show', ['slug' => $shop->getSlug()], Response::HTTP_SEE_OTHER);
  824.         }
  825.         // Récupérer tous les plans pour les contraintes JavaScript
  826.         $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  827.         $plansData = [];
  828.         foreach ($shopPlans as $plan) {
  829.             $plansData[$plan->getId()] = [
  830.                 'id' => $plan->getId(),
  831.                 'name' => $plan->getName(),
  832.                 'requireSiretNif' => $plan->isRequireSiretNif(),
  833.                 'requireIban' => $plan->isRequireIban(),
  834.                 'maxProducts' => $plan->getMaxProducts(),
  835.                 'maxEmployees' => $plan->getMaxEmployees(),
  836.             ];
  837.         }
  838.         
  839.         return $this->render('seller/new_shop_simple.html.twig', [
  840.             'current_menu' => 'shop',
  841.             'current' => 'shopEdit',
  842.             'shop' => $shop,
  843.             'form' => $form->createView(),
  844.             'plansData' => $plansData
  845.         ]);
  846.     }
  847.     #[Route('/shop/{slug}/show/products'name'shop_show_products')]
  848.     public function showProductsSeller(string $slugRequest $requestEntityManagerInterface $em): Response
  849.     {
  850.         if (!$this->getUser()) {
  851.             return $this->redirectToRoute('ui_app_login');
  852.         }
  853.         if (!$this->isAuthorized()) {
  854.             return $this->render('misc/access_denied.html.twig');
  855.         }
  856.         
  857.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  858.         
  859.         if (!$shop) {
  860.             $this->addFlash('error''Boutique non trouvée.');
  861.             return $this->redirectToRoute('seller_index');
  862.         }
  863.         
  864.         // Filtrer uniquement les produits actifs pour le vendeur
  865.         $products $shop->getProducts()->filter(function($product) {
  866.             return $product->isIsActive();
  867.         });
  868.         $categories $em->getRepository(Category::class)->findAll();
  869.         $data = [];
  870.         foreach ($products as $product) {
  871.             $tierPrices $product->getTierPrices();
  872.             $hasTierPricing $product->hasTierPricing() || ($tierPrices && count($tierPrices) > 0);
  873.             
  874.             $data[] = [
  875.                 'id' => $product->getId(),
  876.                 'slug' => $product->getSlug(),
  877.                 'image' => $product->getImages()[0] ?? null,
  878.                 'name' => $product->getName(),
  879.                 'price' => $product->getPrice(),
  880.                 'stock' => $product->getStock(),
  881.                 'status' => $product->getStockStatus(),
  882.                 'hasTierPricing' => $hasTierPricing,
  883.                 'isActive' => $product->isIsActive(),
  884.             ];
  885.         }
  886.         return $this->render('seller/show_products.html.twig', [
  887.             'current_menu' => 'shop',
  888.             'current' => 'products',
  889.             'productsJson' => json_encode($data),
  890.             'categoriesJson' => json_encode(array_map(static function(Category $c){
  891.                 return ['id' => $c->getId(), 'name' => $c->getName()];
  892.             }, $categories)),
  893.             'slugShop' => $shop->getSlug(),
  894.             'shop' => $shop
  895.         ]);
  896.     }
  897.     #[Route('/shop/{slug}/products/new'name'shop_products_new'methods: ['GET''POST'])]
  898.     public function createProductForShop(string $slugRequest $requestEntityManagerInterface $emShopLimitService $shopLimitService): Response
  899.     {
  900.         if (!$this->getUser() || !$this->isAuthorized()) {
  901.             if ($request->isMethod('GET')) {
  902.                 return $this->redirectToRoute('ui_app_login');
  903.             }
  904.             return new JsonResponse(['ok' => false'message' => 'Non autorisé'], 401);
  905.         }
  906.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  907.         
  908.         if (!$shop) {
  909.             if ($request->isMethod('GET')) {
  910.                 $this->addFlash('error''Boutique non trouvée.');
  911.                 return $this->redirectToRoute('seller_index');
  912.             }
  913.             return new JsonResponse(['ok' => false'message' => 'Boutique non trouvée'], 404);
  914.         }
  915.         // Vérifier que la boutique appartient au vendeur
  916.         if (!$shop->getManager()->contains($this->getUser())) {
  917.             if ($request->isMethod('GET')) {
  918.                 $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  919.                 return $this->redirectToRoute('seller_index');
  920.             }
  921.             return new JsonResponse(['ok' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  922.         }
  923.         // Si GET, afficher le formulaire de création
  924.         if ($request->isMethod('GET')) {
  925.             // Vérifier les limites du plan avant d'afficher le formulaire
  926.             $productLimitCheck $shopLimitService->canShopAddProduct($shop);
  927.             $categories $em->getRepository(Category::class)->findAll();
  928.             
  929.             // Récupérer les attributs disponibles pour les variantes
  930.             $attributes $em->getRepository(\App\Entity\ProductAttribute::class)->findActiveOrdered();
  931.             $attributesData = [];
  932.             foreach ($attributes as $attr) {
  933.                 $values = [];
  934.                 foreach ($attr->getValues() as $value) {
  935.                     if ($value->isIsActive()) {
  936.                         $values[] = [
  937.                             'id' => $value->getId(),
  938.                             'value' => $value->getValue(),
  939.                             'colorCode' => $value->getColorCode(),
  940.                             'image' => $value->getImage(),
  941.                         ];
  942.                     }
  943.                 }
  944.                 $attributesData[] = [
  945.                     'id' => $attr->getId(),
  946.                     'name' => $attr->getName(),
  947.                     'slug' => $attr->getSlug(),
  948.                     'type' => $attr->getType(),
  949.                     'values' => $values,
  950.                 ];
  951.             }
  952.             
  953.             return $this->render('seller/product/new.html.twig', [
  954.                 'shop' => $shop,
  955.                 'categories' => $categories,
  956.                 'productLimitCheck' => $productLimitCheck,
  957.                 'attributes' => $attributesData,
  958.             ]);
  959.         }
  960.         // Si POST, vérifier les limites avant de créer le produit
  961.         $productLimitCheck $shopLimitService->canShopAddProduct($shop);
  962.         if (!$productLimitCheck['allowed']) {
  963.             $isAjax $request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest';
  964.             if ($isAjax) {
  965.                 return new JsonResponse(['ok' => false'message' => $productLimitCheck['message']], 403);
  966.             }
  967.             $this->addFlash('error'$productLimitCheck['message']);
  968.             $categories $em->getRepository(Category::class)->findAll();
  969.             return $this->render('seller/product/new.html.twig', [
  970.                 'shop' => $shop,
  971.                 'categories' => $categories,
  972.                 'productLimitCheck' => $productLimitCheck,
  973.             ]);
  974.         }
  975.         // Si POST, créer le produit
  976.         $product = new Product();
  977.         $name trim((string) $request->request->get('name'));
  978.         $description = (string) $request->request->get('description');
  979.         $price = (float) $request->request->get('price');
  980.         $compareAtPrice $request->request->get('compareAtPrice') !== null ? (float) $request->request->get('compareAtPrice') : null;
  981.         $stock = (int) $request->request->get('stock');
  982.         $minStockAlert $request->request->get('minStockAlert') !== null ? (int) $request->request->get('minStockAlert') : 0;
  983.         $manageStock = (bool) $request->request->get('manageStock');
  984.         $allowBackorders = (bool) $request->request->get('allowBackorders');
  985.         $isFeatured = (bool) $request->request->get('isFeatured');
  986.         $isDigital = (bool) $request->request->get('isDigital');
  987.         $stockStatus = (string) $request->request->get('stockStatus');
  988.         $categoryId = (int) $request->request->get('category');
  989.         // Définir $isAjax pour les validations
  990.         $isAjax $request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest';
  991.         if ($name === '' || $price <= || $stock || !$categoryId) {
  992.             if ($isAjax) {
  993.                 return new JsonResponse(['ok' => false'message' => 'Champs requis manquants'], 400);
  994.             }
  995.             $this->addFlash('error''Champs requis manquants ou invalides.');
  996.             return $this->redirectToRoute('seller_shop_products_new', ['slug' => $shop->getSlug()]);
  997.         }
  998.         $category $em->getRepository(Category::class)->find($categoryId);
  999.         if (!$category) {
  1000.             if ($isAjax) {
  1001.                 return new JsonResponse(['ok' => false'message' => 'Catégorie invalide'], 400);
  1002.             }
  1003.             $this->addFlash('error''Catégorie invalide.');
  1004.             return $this->redirectToRoute('seller_shop_products_new', ['slug' => $shop->getSlug()]);
  1005.         }
  1006.         $slugger = new AsciiSlugger();
  1007.         $slug strtolower($slugger->slug($name)->toString());
  1008.         $sku 'SKU-' strtoupper(substr(md5($name.microtime()), 08));
  1009.         $product->setName($name);
  1010.         $product->setDescription($description ?: null);
  1011.         $product->setPrice($price);
  1012.         $product->setCompareAtPrice($compareAtPrice);
  1013.         $product->setStock($stock);
  1014.         $product->setMinStockAlert($minStockAlert);
  1015.         $product->setManageStock($manageStock);
  1016.         $product->setAllowBackorders($allowBackorders);
  1017.         $product->setIsFeatured($isFeatured);
  1018.         $product->setIsDigital($isDigital);
  1019.         $product->setStockStatus($stockStatus ?: 'In stock');
  1020.         $product->setCategory($category);
  1021.         $product->setShop($shop);
  1022.         $product->setSlug($slug);
  1023.         $product->setSku($sku);
  1024.         $product->setIsActive(true);
  1025.         $product->setPublishedAt(new \DateTimeImmutable('now'));
  1026.         $product->setViewCount(0);
  1027.         $product->setSalesCount(0);
  1028.         $product->setAverageRating(0);
  1029.         $product->setReviewCount(0);
  1030.         // Prix de gros (facultatif, à l'initiative du vendeur)
  1031.         $enableTierPricing = (bool) $request->request->get('enableTierPricing');
  1032.         $tierMins $request->request->all('tierMin');
  1033.         $tierPrices $request->request->all('tierPrice');
  1034.         $normalizedTiers = [];
  1035.         if ($enableTierPricing && is_array($tierMins) && is_array($tierPrices)) {
  1036.             $count min(count($tierMins), count($tierPrices));
  1037.             for ($i 0$i $count$i++) {
  1038.                 $m = (int) ($tierMins[$i] ?? 0);
  1039.                 $p = (float) ($tierPrices[$i] ?? 0);
  1040.                 if ($m >= && $p 0) {
  1041.                     $normalizedTiers[$m] = $p// utiliser la quantité min comme clé pour dédupliquer
  1042.                 }
  1043.             }
  1044.             if (!empty($normalizedTiers)) {
  1045.                 ksort($normalizedTiersSORT_NUMERIC);
  1046.                 $tiersArray = [];
  1047.                 foreach ($normalizedTiers as $minQty => $priceVal) {
  1048.                     $tiersArray[] = [ 'min' => (int)$minQty'price' => (float)$priceVal ];
  1049.                 }
  1050.                 $product->setHasTierPricing(true);
  1051.                 $product->setTierPrices($tiersArray);
  1052.             } else {
  1053.                 $product->setHasTierPricing(false);
  1054.                 $product->setTierPrices(null);
  1055.             }
  1056.         } else {
  1057.             $product->setHasTierPricing(false);
  1058.             $product->setTierPrices(null);
  1059.         }
  1060.         // Définir $isAjax pour les blocs catch
  1061.         $isAjax $request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest';
  1062.         // Uploads
  1063.         try {
  1064.             $projectDir $this->getParameter('kernel.project_dir');
  1065.             $baseUploadDir $projectDir '/public/uploads/products/' $shop->getSlug();
  1066.             $imagesDir $baseUploadDir '/images';
  1067.             $videosDir $baseUploadDir '/videos';
  1068.             $documentsDir $baseUploadDir '/documents';
  1069.             foreach ([$imagesDir$videosDir$documentsDir] as $dir) {
  1070.                 if (!is_dir($dir) && !@mkdir($dir0777true) && !is_dir($dir)) {
  1071.                     throw new \RuntimeException('Impossible de créer le dossier: ' $dir);
  1072.                 }
  1073.             }
  1074.             $sluggerFile = new AsciiSlugger();
  1075.             $images = [];
  1076.             $filesImages $request->files->get('images', []);
  1077.             if ($filesImages instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  1078.                 $filesImages = [$filesImages];
  1079.             }
  1080.             foreach ($filesImages as $file) {
  1081.                 if (!$file || !($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) || !$file->isValid()) continue;
  1082.                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  1083.                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  1084.                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  1085.                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  1086.                 $filename $safeOrig '_' $unique '.' $ext;
  1087.                 $file->move($imagesDir$filename);
  1088.                 $images[] = '/uploads/products/' $shop->getSlug() . '/images/' $filename;
  1089.             }
  1090.             $videos = [];
  1091.             $filesVideos $request->files->get('videos', []);
  1092.             if ($filesVideos instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  1093.                 $filesVideos = [$filesVideos];
  1094.             }
  1095.             foreach ($filesVideos as $file) {
  1096.                 if (!$file || !($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) || !$file->isValid()) continue;
  1097.                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  1098.                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  1099.                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  1100.                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  1101.                 $filename $safeOrig '_' $unique '.' $ext;
  1102.                 $file->move($videosDir$filename);
  1103.                 $videos[] = '/uploads/products/' $shop->getSlug() . '/videos/' $filename;
  1104.             }
  1105.             $documents = [];
  1106.             $filesDocuments $request->files->get('documents', []);
  1107.             if ($filesDocuments instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  1108.                 $filesDocuments = [$filesDocuments];
  1109.             }
  1110.             foreach ($filesDocuments as $file) {
  1111.                 if (!$file || !($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) || !$file->isValid()) continue;
  1112.                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  1113.                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  1114.                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  1115.                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  1116.                 $filename $safeOrig '_' $unique '.' $ext;
  1117.                 $file->move($documentsDir$filename);
  1118.                 $documents[] = '/uploads/products/' $shop->getSlug() . '/documents/' $filename;
  1119.             }
  1120.         } catch (\Throwable $e) {
  1121.             if ($isAjax) {
  1122.                 return new JsonResponse(['ok' => false'message' => 'Upload échoué: '.$e->getMessage()], 500);
  1123.             }
  1124.             $this->addFlash('error''Erreur lors de l\'upload des fichiers: '.$e->getMessage());
  1125.             return $this->redirectToRoute('seller_shop_products_new', ['slug' => $shop->getSlug()]);
  1126.         }
  1127.         $product->setImages($images);
  1128.         $product->setVideos($videos ?: null);
  1129.         $product->setDocuments($documents ?: null);
  1130.         // Gérer les variantes de produit
  1131.         $variantsData $request->request->all('variants') ?? [];
  1132.         $variantsFiles $request->files->all('variants') ?? [];
  1133.         $attributeValueRepo $em->getRepository(\App\Entity\ProductAttributeValue::class);
  1134.         
  1135.         foreach ($variantsData as $variantIndex => $variantData) {
  1136.             if (empty($variantData['price']) || empty($variantData['stock'])) {
  1137.                 continue; // Ignorer les variantes incomplètes
  1138.             }
  1139.             
  1140.             $variant = new \App\Entity\ProductVariant();
  1141.             $variant->setProduct($product);
  1142.             // Générer automatiquement le SKU si non fourni
  1143.             if (empty($variantData['sku'])) {
  1144.                 $variantSku 'SKU-' strtoupper(substr(md5($name microtime() . $variantIndex), 08)) . '-' uniqid();
  1145.                 $variant->setSku($variantSku);
  1146.             } else {
  1147.                 $variant->setSku($variantData['sku']);
  1148.             }
  1149.             $variant->setPrice((float) $variantData['price']);
  1150.             $variant->setStock((int) $variantData['stock']);
  1151.             $variant->setCompareAtPrice(!empty($variantData['compareAtPrice']) ? (float) $variantData['compareAtPrice'] : null);
  1152.             $variant->setManageStock($manageStock);
  1153.             $variant->setAllowBackorders($allowBackorders);
  1154.             $variant->setStockStatus($stockStatus ?: 'in_stock');
  1155.             $variant->setIsActive(true);
  1156.             $variant->setIsDefault(false);
  1157.             
  1158.             // Gérer les attributs de la variante
  1159.             if (!empty($variantData['attributes']) && is_array($variantData['attributes'])) {
  1160.                 foreach ($variantData['attributes'] as $attrData) {
  1161.                     if (!empty($attrData['valueId'])) {
  1162.                         $attrValue $attributeValueRepo->find((int) $attrData['valueId']);
  1163.                         if ($attrValue && $attrValue->isIsActive()) {
  1164.                             $variant->addAttributeValue($attrValue);
  1165.                         }
  1166.                     }
  1167.                 }
  1168.             }
  1169.             
  1170.             // Gérer les images spécifiques à la variante
  1171.             $variantImages = [];
  1172.             if (isset($variantsFiles[$variantIndex]['images']) && is_array($variantsFiles[$variantIndex]['images'])) {
  1173.                 foreach ($variantsFiles[$variantIndex]['images'] as $file) {
  1174.                     if ($file && $file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile && $file->isValid()) {
  1175.                         $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  1176.                         $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  1177.                         $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  1178.                         $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  1179.                         $filename $safeOrig '_' $unique '.' $ext;
  1180.                         $file->move($imagesDir$filename);
  1181.                         $variantImages[] = '/uploads/products/' $shop->getSlug() . '/images/' $filename;
  1182.                     }
  1183.                 }
  1184.             }
  1185.             $variant->setImages($variantImages);
  1186.             
  1187.             $em->persist($variant);
  1188.         }
  1189.         
  1190.         // Gérer les détails supplémentaires
  1191.         $detailsData $request->request->all('details') ?? [];
  1192.         foreach ($detailsData as $detailData) {
  1193.             if (empty($detailData['label']) || empty($detailData['value'])) {
  1194.                 continue; // Ignorer les détails incomplets
  1195.             }
  1196.             
  1197.             $detail = new \App\Entity\ProductDetail();
  1198.             $detail->setProduct($product);
  1199.             $detail->setLabel(trim($detailData['label']));
  1200.             $detail->setValue(trim($detailData['value']));
  1201.             $detail->setIsActive(true);
  1202.             $detail->setSortOrder(0);
  1203.             
  1204.             $em->persist($detail);
  1205.         }
  1206.         try {
  1207.             // Incrémente le compteur de produits de la catégorie sélectionnée
  1208.             if ($category) {
  1209.                 $category->incrementProductCount();
  1210.                 $em->persist($category);
  1211.             }
  1212.             $em->persist($product);
  1213.             $em->flush();
  1214.             
  1215.             // Mettre à jour le compteur de produits de la boutique
  1216.             $shopLimitService->updateProductCount($shop);
  1217.         } catch (\Throwable $e) {
  1218.             if ($isAjax) {
  1219.                 return new JsonResponse(['ok' => false'message' => 'Erreur enregistrement: '.$e->getMessage()], 500);
  1220.             }
  1221.             $this->addFlash('error''Erreur lors de l\'enregistrement: '.$e->getMessage());
  1222.             return $this->redirectToRoute('seller_shop_products_new', ['slug' => $shop->getSlug()]);
  1223.         }
  1224.         // Rediriger vers la page d'édition du produit créé
  1225.         if ($isAjax) {
  1226.             return new JsonResponse([
  1227.                 'ok' => true,
  1228.                 'redirect' => $this->generateUrl('seller_product_edit', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]),
  1229.                 'product' => [
  1230.                     'id' => $product->getId(),
  1231.                     'image' => $images[0] ?? null,
  1232.                     'name' => $product->getName(),
  1233.                     'price' => $product->getPrice(),
  1234.                     'stock' => $product->getStock(),
  1235.                     'status' => $product->getStockStatus(),
  1236.                     'hasTierPricing' => $product->hasTierPricing(),
  1237.                 ]
  1238.             ]);
  1239.         }
  1240.         
  1241.         $this->addFlash('success''Produit créé avec succès !');
  1242.         return $this->redirectToRoute('seller_shop_show', ['slug' => $shop->getSlug()]);
  1243.     }
  1244.     #[Route('/shop/{shopSlug}/products/{id}/show'name'product_show')]
  1245.     // Note: Le préfixe 'seller_' fait que la route finale est 'seller_product_show'
  1246.     public function showProductSeller(string $shopSlugint $idEntityManagerInterface $em): Response
  1247.     {
  1248.         if (!$this->getUser() || !$this->isAuthorized()) {
  1249.             return $this->redirectToRoute('ui_app_login');
  1250.         }
  1251.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]);
  1252.         
  1253.         if (!$shop) {
  1254.             $this->addFlash('error''Boutique non trouvée.');
  1255.             return $this->redirectToRoute('seller_index');
  1256.         }
  1257.         // Vérifier que la boutique appartient au vendeur
  1258.         if (!$shop->getManager()->contains($this->getUser())) {
  1259.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  1260.             return $this->redirectToRoute('seller_index');
  1261.         }
  1262.         $product $em->getRepository(Product::class)->find($id);
  1263.         
  1264.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  1265.             throw $this->createNotFoundException('Produit non trouvé');
  1266.         }
  1267.         // Rendre le template seller pour voir le produit
  1268.         return $this->render('seller/product/show.html.twig', [
  1269.             'product' => $product,
  1270.             'shop' => $shop,
  1271.         ]);
  1272.     }
  1273.     #[Route('/shop/{shopSlug}/products/{id}/edit'name'product_edit'methods: ['GET''POST'])]
  1274.     // Note: Le préfixe 'seller_' fait que la route finale est 'seller_product_edit'
  1275.     public function editProductSeller(string $shopSlugint $idRequest $requestEntityManagerInterface $em): Response
  1276.     {
  1277.         if (!$this->getUser() || !$this->isAuthorized()) {
  1278.             return $this->redirectToRoute('ui_app_login');
  1279.         }
  1280.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]);
  1281.         
  1282.         if (!$shop) {
  1283.             $this->addFlash('error''Boutique non trouvée.');
  1284.             return $this->redirectToRoute('seller_index');
  1285.         }
  1286.         // Vérifier que la boutique appartient au vendeur
  1287.         if (!$shop->getManager()->contains($this->getUser())) {
  1288.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  1289.             return $this->redirectToRoute('seller_index');
  1290.         }
  1291.         $product $em->getRepository(Product::class)->find($id);
  1292.         
  1293.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  1294.             throw $this->createNotFoundException('Produit non trouvé');
  1295.         }
  1296.         // Traitement de la soumission du formulaire (POST)
  1297.         if ($request->isMethod('POST')) {
  1298.             $isAjax $request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest';
  1299.             
  1300.             $name trim((string) $request->request->get('name'));
  1301.             $description = (string) $request->request->get('description');
  1302.             $price = (float) $request->request->get('price');
  1303.             $compareAtPrice $request->request->get('compareAtPrice') !== null && $request->request->get('compareAtPrice') !== '' ? (float) $request->request->get('compareAtPrice') : null;
  1304.             $stock = (int) $request->request->get('stock');
  1305.             $minStockAlert $request->request->get('minStockAlert') !== null ? (int) $request->request->get('minStockAlert') : 0;
  1306.             // Valeurs par défaut gérées en backend
  1307.             $manageStock true// Gérer le stock par défaut
  1308.             $allowBackorders false// Pas de précommandes par défaut
  1309.             $isFeatured false// Pas en vedette par défaut
  1310.             $stockStatus 'In stock'// En stock par défaut
  1311.             $isDigital = (bool) $request->request->get('isDigital');
  1312.             $categoryId = (int) $request->request->get('category');
  1313.             $isActive = (bool) $request->request->get('isActive');
  1314.             // Si c'est un upload partiel via AJAX, on peut ignorer la validation des champs de base
  1315.             // à condition que des fichiers soient présents
  1316.             $uploadedImages $request->files->all('images');
  1317.             $uploadedVideos $request->files->all('videos');
  1318.             $uploadedDocuments $request->files->all('documents');
  1319.             $isPartialUpload $isAjax && (!empty($uploadedImages) || !empty($uploadedVideos) || !empty($uploadedDocuments));
  1320.             if (!$isPartialUpload) {
  1321.             if ($name === '' || $price <= || $stock || !$categoryId) {
  1322.                     if ($isAjax) {
  1323.                         return new JsonResponse(['success' => false'message' => 'Champs requis manquants ou invalides.'], 400);
  1324.                     }
  1325.                 $this->addFlash('error''Champs requis manquants ou invalides.');
  1326.                 return $this->redirectToRoute('seller_product_edit', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]);
  1327.             }
  1328.             $category $em->getRepository(Category::class)->find($categoryId);
  1329.             if (!$category) {
  1330.                     if ($isAjax) {
  1331.                         return new JsonResponse(['success' => false'message' => 'Catégorie invalide.'], 400);
  1332.                     }
  1333.                 $this->addFlash('error''Catégorie invalide.');
  1334.                 return $this->redirectToRoute('seller_product_edit', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]);
  1335.             }
  1336.             // Mettre à jour le slug si le nom a changé
  1337.             if ($product->getName() !== $name) {
  1338.                 $slugger = new AsciiSlugger();
  1339.                 $slug strtolower($slugger->slug($name)->toString());
  1340.                 $product->setSlug($slug);
  1341.             }
  1342.             $product->setName($name);
  1343.             $product->setDescription($description ?: null);
  1344.             $product->setPrice($price);
  1345.             $product->setCompareAtPrice($compareAtPrice);
  1346.             $product->setStock($stock);
  1347.             $product->setMinStockAlert($minStockAlert);
  1348.             $product->setManageStock($manageStock);
  1349.             $product->setAllowBackorders($allowBackorders);
  1350.             $product->setIsFeatured($isFeatured);
  1351.             $product->setIsDigital($isDigital);
  1352.             $product->setStockStatus($stockStatus ?: 'In stock');
  1353.             $product->setCategory($category);
  1354.             $product->setIsActive($isActive);
  1355.             // Prix de gros (Tier Pricing)
  1356.             $enableTierPricing = (bool) $request->request->get('enableTierPricing');
  1357.             $tierMins $request->request->all('tierMin');
  1358.             $tierPrices $request->request->all('tierPrice');
  1359.             $normalizedTiers = [];
  1360.             if ($enableTierPricing && is_array($tierMins) && is_array($tierPrices)) {
  1361.                 $count min(count($tierMins), count($tierPrices));
  1362.                 for ($i 0$i $count$i++) {
  1363.                     $m = (int) ($tierMins[$i] ?? 0);
  1364.                     $p = (float) ($tierPrices[$i] ?? 0);
  1365.                     if ($m >= && $p 0) {
  1366.                         $normalizedTiers[$m] = $p;
  1367.                     }
  1368.                 }
  1369.                 if (!empty($normalizedTiers)) {
  1370.                     ksort($normalizedTiersSORT_NUMERIC);
  1371.                     $tiersArray = [];
  1372.                     foreach ($normalizedTiers as $minQty => $priceVal) {
  1373.                         $tiersArray[] = [ 'min' => (int)$minQty'price' => (float)$priceVal ];
  1374.                     }
  1375.                     $product->setHasTierPricing(true);
  1376.                     $product->setTierPrices($tiersArray);
  1377.                 } else {
  1378.                     $product->setHasTierPricing(false);
  1379.                     $product->setTierPrices(null);
  1380.                 }
  1381.             } else {
  1382.                 $product->setHasTierPricing(false);
  1383.                 $product->setTierPrices(null);
  1384.                 }
  1385.             }
  1386.             // Gestion des fichiers (images, vidéos, documents)
  1387.             $images $product->getImages();
  1388.             $videos $product->getVideos();
  1389.             $documents $product->getDocuments();
  1390.             // Upload des nouvelles images
  1391.             if (!empty($uploadedImages)) {
  1392.                 $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId();
  1393.                 if (!is_dir($productDir)) {
  1394.                     mkdir($productDir0755true);
  1395.                 }
  1396.                 foreach ($uploadedImages as $file) {
  1397.                     if ($file && $file->isValid()) {
  1398.                         $filename uniqid() . '.' $file->guessExtension();
  1399.                         $file->move($productDir$filename);
  1400.                         $images[] = '/uploads/products/' $product->getId() . '/' $filename;
  1401.                     }
  1402.                 }
  1403.             }
  1404.             // Upload des nouvelles vidéos
  1405.             $uploadedVideos $request->files->all('videos');
  1406.             if (!empty($uploadedVideos)) {
  1407.                 if ($videos === null$videos = [];
  1408.                 $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId() . '/videos';
  1409.                 if (!is_dir($productDir)) {
  1410.                     mkdir($productDir0755true);
  1411.                 }
  1412.                 foreach ($uploadedVideos as $file) {
  1413.                     if ($file && $file->isValid()) {
  1414.                         $filename uniqid() . '.' $file->guessExtension();
  1415.                         $file->move($productDir$filename);
  1416.                         $videos[] = '/uploads/products/' $product->getId() . '/videos/' $filename;
  1417.                     }
  1418.                 }
  1419.             }
  1420.             // Upload des nouveaux documents
  1421.             $uploadedDocuments $request->files->all('documents');
  1422.             if (!empty($uploadedDocuments)) {
  1423.                 if ($documents === null$documents = [];
  1424.                 $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId() . '/documents';
  1425.                 if (!is_dir($productDir)) {
  1426.                     mkdir($productDir0755true);
  1427.                 }
  1428.                 foreach ($uploadedDocuments as $file) {
  1429.                     if ($file && $file->isValid()) {
  1430.                         $filename uniqid() . '.' $file->guessExtension();
  1431.                         $file->move($productDir$filename);
  1432.                         $documents[] = '/uploads/products/' $product->getId() . '/documents/' $filename;
  1433.                     }
  1434.                 }
  1435.             }
  1436.             $product->setImages($images);
  1437.             $product->setVideos($videos ?: null);
  1438.             $product->setDocuments($documents ?: null);
  1439.             // Gérer les variantes de produit
  1440.             $variantsData $request->request->all('variants') ?? [];
  1441.             $variantsFiles $request->files->all('variants') ?? [];
  1442.             $attributeValueRepo $em->getRepository(\App\Entity\ProductAttributeValue::class);
  1443.             $variantRepo $em->getRepository(\App\Entity\ProductVariant::class);
  1444.             $sluggerFile = new AsciiSlugger();
  1445.             
  1446.             // Récupérer toutes les variantes existantes
  1447.             $existingVariants = [];
  1448.             foreach ($product->getVariants() as $existingVariant) {
  1449.                 $existingVariants[$existingVariant->getId()] = $existingVariant;
  1450.             }
  1451.             
  1452.             // Traiter les variantes soumises
  1453.             foreach ($variantsData as $variantKey => $variantData) {
  1454.                 // Vérifier si c'est une suppression
  1455.                 if (!empty($variantData['delete'])) {
  1456.                     if (isset($existingVariants[$variantKey])) {
  1457.                         $existingVariants[$variantKey]->setIsActive(false);
  1458.                     }
  1459.                     continue;
  1460.                 }
  1461.                 
  1462.                 // Vérifier si c'est une variante existante à mettre à jour
  1463.                 if (is_numeric($variantKey) && isset($existingVariants[$variantKey])) {
  1464.                     $variant $existingVariants[$variantKey];
  1465.                     // Le SKU est préservé via le champ caché, pas besoin de le modifier
  1466.                     $variant->setPrice((float) $variantData['price']);
  1467.                     $variant->setStock((int) $variantData['stock']);
  1468.                     $variant->setCompareAtPrice(!empty($variantData['compareAtPrice']) ? (float) $variantData['compareAtPrice'] : null);
  1469.                     
  1470.                     // Mettre à jour les attributs
  1471.                     $variant->getAttributeValues()->clear();
  1472.                     if (!empty($variantData['attributes']) && is_array($variantData['attributes'])) {
  1473.                         foreach ($variantData['attributes'] as $attrData) {
  1474.                             if (!empty($attrData['valueId'])) {
  1475.                                 $attrValue $attributeValueRepo->find((int) $attrData['valueId']);
  1476.                                 if ($attrValue && $attrValue->isIsActive()) {
  1477.                                     $variant->addAttributeValue($attrValue);
  1478.                                 }
  1479.                             }
  1480.                         }
  1481.                     }
  1482.                     
  1483.                     // Gérer les nouvelles images de variante
  1484.                     if (isset($variantsFiles[$variantKey]['images']) && is_array($variantsFiles[$variantKey]['images'])) {
  1485.                         $variantImages $variant->getImages();
  1486.                         $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId();
  1487.                         foreach ($variantsFiles[$variantKey]['images'] as $file) {
  1488.                             if ($file && $file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile && $file->isValid()) {
  1489.                                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  1490.                                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  1491.                                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  1492.                                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  1493.                                 $filename $safeOrig '_' $unique '.' $ext;
  1494.                                 $file->move($productDir$filename);
  1495.                                 $variantImages[] = '/uploads/products/' $product->getId() . '/' $filename;
  1496.                             }
  1497.                         }
  1498.                         $variant->setImages($variantImages);
  1499.                     }
  1500.                 } else {
  1501.                     // Nouvelle variante
  1502.                     if (empty($variantData['price']) || empty($variantData['stock'])) {
  1503.                         continue;
  1504.                     }
  1505.                     
  1506.                     $variant = new \App\Entity\ProductVariant();
  1507.                     $variant->setProduct($product);
  1508.                     // Générer automatiquement le SKU si non fourni
  1509.                     if (empty($variantData['sku'])) {
  1510.                         $variantSku 'SKU-' strtoupper(substr(md5($name microtime() . $variantKey), 08)) . '-' uniqid();
  1511.                         $variant->setSku($variantSku);
  1512.                     } else {
  1513.                         $variant->setSku($variantData['sku']);
  1514.                     }
  1515.                     $variant->setPrice((float) $variantData['price']);
  1516.                     $variant->setStock((int) $variantData['stock']);
  1517.                     $variant->setCompareAtPrice(!empty($variantData['compareAtPrice']) ? (float) $variantData['compareAtPrice'] : null);
  1518.                     $variant->setManageStock($manageStock);
  1519.                     $variant->setAllowBackorders($allowBackorders);
  1520.                     $variant->setStockStatus($stockStatus ?: 'in_stock');
  1521.                     $variant->setIsActive(true);
  1522.                     $variant->setIsDefault(false);
  1523.                     
  1524.                     // Gérer les attributs
  1525.                     if (!empty($variantData['attributes']) && is_array($variantData['attributes'])) {
  1526.                         foreach ($variantData['attributes'] as $attrData) {
  1527.                             if (!empty($attrData['valueId'])) {
  1528.                                 $attrValue $attributeValueRepo->find((int) $attrData['valueId']);
  1529.                                 if ($attrValue && $attrValue->isIsActive()) {
  1530.                                     $variant->addAttributeValue($attrValue);
  1531.                                 }
  1532.                             }
  1533.                         }
  1534.                     }
  1535.                     
  1536.                     // Gérer les images
  1537.                     $variantImages = [];
  1538.                     if (isset($variantsFiles[$variantKey]['images']) && is_array($variantsFiles[$variantKey]['images'])) {
  1539.                         $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId();
  1540.                         foreach ($variantsFiles[$variantKey]['images'] as $file) {
  1541.                             if ($file && $file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile && $file->isValid()) {
  1542.                                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  1543.                                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  1544.                                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  1545.                                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  1546.                                 $filename $safeOrig '_' $unique '.' $ext;
  1547.                                 $file->move($productDir$filename);
  1548.                                 $variantImages[] = '/uploads/products/' $product->getId() . '/' $filename;
  1549.                             }
  1550.                         }
  1551.                     }
  1552.                     $variant->setImages($variantImages);
  1553.                     
  1554.                     $em->persist($variant);
  1555.                 }
  1556.             }
  1557.             
  1558.             // Supprimer les variantes qui n'ont pas été soumises (si nécessaire)
  1559.             // Pour l'instant, on garde toutes les variantes existantes sauf celles marquées pour suppression
  1560.             
  1561.             // Gérer les détails supplémentaires
  1562.             $detailsData $request->request->all('details') ?? [];
  1563.             $detailRepo $em->getRepository(\App\Entity\ProductDetail::class);
  1564.             
  1565.             // Récupérer tous les détails existants
  1566.             $existingDetails = [];
  1567.             foreach ($product->getDetails() as $existingDetail) {
  1568.                 $existingDetails[$existingDetail->getId()] = $existingDetail;
  1569.             }
  1570.             
  1571.             // Traiter les détails soumis
  1572.             foreach ($detailsData as $detailKey => $detailData) {
  1573.                 // Vérifier si c'est une suppression
  1574.                 if (!empty($detailData['delete'])) {
  1575.                     if (isset($existingDetails[$detailKey])) {
  1576.                         $existingDetails[$detailKey]->setIsActive(false);
  1577.                     }
  1578.                     continue;
  1579.                 }
  1580.                 
  1581.                 // Vérifier si c'est un détail existant à mettre à jour
  1582.                 if (is_numeric($detailKey) && isset($existingDetails[$detailKey])) {
  1583.                     $detail $existingDetails[$detailKey];
  1584.                     $detail->setLabel(trim($detailData['label']));
  1585.                     $detail->setValue(trim($detailData['value']));
  1586.                 } else {
  1587.                     // Nouveau détail
  1588.                     if (empty($detailData['label']) || empty($detailData['value'])) {
  1589.                         continue;
  1590.                     }
  1591.                     
  1592.                     $detail = new \App\Entity\ProductDetail();
  1593.                     $detail->setProduct($product);
  1594.                     $detail->setLabel(trim($detailData['label']));
  1595.                     $detail->setValue(trim($detailData['value']));
  1596.                     $detail->setIsActive(true);
  1597.                     $detail->setSortOrder(0);
  1598.                     
  1599.                     $em->persist($detail);
  1600.                 }
  1601.             }
  1602.             try {
  1603.                 $em->flush();
  1604.                 
  1605.                 // Si c'est une requête AJAX (upload de fichiers), retourner du JSON
  1606.                 if ($request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest' || !empty($uploadedImages) || !empty($uploadedVideos) || !empty($uploadedDocuments)) {
  1607.                     // Si la requête contient uniquement des fichiers, on peut considérer que c'est un upload partiel
  1608.                     // Sinon, c'est peut-être la soumission globale via AJAX
  1609.                     return new JsonResponse([
  1610.                         'success' => true,
  1611.                         'message' => 'Produit mis à jour avec succès',
  1612.                         'data' => [
  1613.                             'images' => $product->getImages(),
  1614.                             'videos' => $product->getVideos(),
  1615.                             'documents' => $product->getDocuments()
  1616.                         ]
  1617.                     ]);
  1618.                 }
  1619.                 $this->addFlash('success''Produit modifié avec succès.');
  1620.                 return $this->redirectToRoute('seller_product_show', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]);
  1621.             } catch (\Throwable $e) {
  1622.                 if ($request->isXmlHttpRequest() || $request->headers->get('X-Requested-With') === 'XMLHttpRequest') {
  1623.                     return new JsonResponse(['success' => false'message' => 'Erreur lors de la modification: ' $e->getMessage()], 500);
  1624.                 }
  1625.                 $this->addFlash('error''Erreur lors de la modification: ' $e->getMessage());
  1626.             }
  1627.         }
  1628.         // Récupérer les catégories pour le formulaire
  1629.         $categories $em->getRepository(Category::class)->findAll();
  1630.         // Récupérer les attributs disponibles pour les variantes
  1631.         $attributes $em->getRepository(\App\Entity\ProductAttribute::class)->findActiveOrdered();
  1632.         $attributesData = [];
  1633.         foreach ($attributes as $attr) {
  1634.             $values = [];
  1635.             foreach ($attr->getValues() as $value) {
  1636.                 if ($value->isIsActive()) {
  1637.                     $values[] = [
  1638.                         'id' => $value->getId(),
  1639.                         'value' => $value->getValue(),
  1640.                         'colorCode' => $value->getColorCode(),
  1641.                         'image' => $value->getImage(),
  1642.                     ];
  1643.                 }
  1644.             }
  1645.             $attributesData[] = [
  1646.                 'id' => $attr->getId(),
  1647.                 'name' => $attr->getName(),
  1648.                 'slug' => $attr->getSlug(),
  1649.                 'type' => $attr->getType(),
  1650.                 'values' => $values,
  1651.             ];
  1652.         }
  1653.         // Rendre le template seller pour modifier le produit
  1654.         return $this->render('seller/product/edit.html.twig', [
  1655.             'product' => $product,
  1656.             'shop' => $shop,
  1657.             'categories' => $categories,
  1658.             'attributes' => $attributesData,
  1659.         ]);
  1660.     }
  1661.     #[Route('/shop/{shopSlug}/products/{id}/image/delete'name'product_image_delete'methods: ['POST'])]
  1662.     public function deleteProductImage(string $shopSlugint $idRequest $requestEntityManagerInterface $em): JsonResponse
  1663.     {
  1664.         if (!$this->getUser() || !$this->isAuthorized()) {
  1665.             return new JsonResponse(['success' => false'message' => 'Non autorisé'], 401);
  1666.         }
  1667.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]);
  1668.         
  1669.         if (!$shop) {
  1670.             return new JsonResponse(['success' => false'message' => 'Boutique non trouvée'], 404);
  1671.         }
  1672.         // Vérifier que la boutique appartient au vendeur
  1673.         if (!$shop->getManager()->contains($this->getUser())) {
  1674.             return new JsonResponse(['success' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  1675.         }
  1676.         $product $em->getRepository(Product::class)->find($id);
  1677.         
  1678.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  1679.             return new JsonResponse(['success' => false'message' => 'Produit non trouvé'], 404);
  1680.         }
  1681.         $imagePath $request->request->get('image');
  1682.         if (!$imagePath) {
  1683.             return new JsonResponse(['success' => false'message' => 'Chemin d\'image manquant'], 400);
  1684.         }
  1685.         // Normaliser le chemin (s'assurer qu'il commence par /)
  1686.         $normalizedPath ltrim($imagePath'/');
  1687.         $normalizedPath '/' $normalizedPath;
  1688.         $images $product->getImages();
  1689.         
  1690.         // Essayer de trouver l'image avec différentes variantes du chemin
  1691.         $imageIndex false;
  1692.         foreach ($images as $index => $storedImage) {
  1693.             // Normaliser le chemin stocké aussi
  1694.             $normalizedStored ltrim($storedImage'/');
  1695.             $normalizedStored '/' $normalizedStored;
  1696.             
  1697.             // Comparer les chemins normalisés ou originaux
  1698.             if ($normalizedStored === $normalizedPath || 
  1699.                 $storedImage === $imagePath || 
  1700.                 $storedImage === $normalizedPath ||
  1701.                 $storedImage === ltrim($imagePath'/')) {
  1702.                 $imageIndex $index;
  1703.                 $imagePath $storedImage// Utiliser le chemin exact stocké
  1704.                 break;
  1705.             }
  1706.         }
  1707.         
  1708.         if ($imageIndex === false) {
  1709.             return new JsonResponse([
  1710.                 'success' => false
  1711.                 'message' => 'Image non trouvée dans la liste',
  1712.                 'debug' => [
  1713.                     'requested' => $imagePath,
  1714.                     'normalized_requested' => $normalizedPath,
  1715.                     'available_images' => $images
  1716.                 ]
  1717.             ], 404);
  1718.         }
  1719.         // Supprimer l'image du tableau
  1720.         unset($images[$imageIndex]);
  1721.         $images array_values($images); // Réindexer le tableau
  1722.         $product->setImages($images);
  1723.         // Supprimer le fichier physique - utiliser le chemin exact stocké
  1724.         $filePathToDelete $this->getParameter('kernel.project_dir') . '/public' $imagePath;
  1725.         
  1726.         // Essayer aussi avec le chemin normalisé si différent
  1727.         if (!file_exists($filePathToDelete) && $imagePath !== $normalizedPath) {
  1728.             $filePathToDelete $this->getParameter('kernel.project_dir') . '/public' $normalizedPath;
  1729.         }
  1730.         
  1731.         if (file_exists($filePathToDelete)) {
  1732.             @unlink($filePathToDelete);
  1733.         }
  1734.         try {
  1735.             $em->flush();
  1736.             return new JsonResponse([
  1737.                 'success' => true,
  1738.                 'message' => 'Image supprimée avec succès',
  1739.                 'data' => [
  1740.                     'images' => $product->getImages(),
  1741.                     'videos' => $product->getVideos(),
  1742.                     'documents' => $product->getDocuments()
  1743.                 ]
  1744.             ]);
  1745.         } catch (\Throwable $e) {
  1746.             return new JsonResponse(['success' => false'message' => 'Erreur lors de la suppression: ' $e->getMessage()], 500);
  1747.         }
  1748.     }
  1749.     #[Route('/shop/{shopSlug}/products/{id}/video/delete'name'product_video_delete'methods: ['POST'])]
  1750.     public function deleteProductVideo(string $shopSlugint $idRequest $requestEntityManagerInterface $em): JsonResponse
  1751.     {
  1752.         if (!$this->getUser() || !$this->isAuthorized()) {
  1753.             return new JsonResponse(['success' => false'message' => 'Non autorisé'], 401);
  1754.         }
  1755.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]);
  1756.         
  1757.         if (!$shop) {
  1758.             return new JsonResponse(['success' => false'message' => 'Boutique non trouvée'], 404);
  1759.         }
  1760.         if (!$shop->getManager()->contains($this->getUser())) {
  1761.             return new JsonResponse(['success' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  1762.         }
  1763.         $product $em->getRepository(Product::class)->find($id);
  1764.         
  1765.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  1766.             return new JsonResponse(['success' => false'message' => 'Produit non trouvé'], 404);
  1767.         }
  1768.         $videoPath $request->request->get('video');
  1769.         if (!$videoPath) {
  1770.             return new JsonResponse(['success' => false'message' => 'Chemin de vidéo manquant'], 400);
  1771.         }
  1772.         $normalizedPath ltrim($videoPath'/');
  1773.         $normalizedPath '/' $normalizedPath;
  1774.         $videos $product->getVideos() ?: [];
  1775.         
  1776.         $videoIndex false;
  1777.         foreach ($videos as $index => $storedVideo) {
  1778.             $normalizedStored ltrim($storedVideo'/');
  1779.             $normalizedStored '/' $normalizedStored;
  1780.             
  1781.             if ($normalizedStored === $normalizedPath || 
  1782.                 $storedVideo === $videoPath || 
  1783.                 $storedVideo === $normalizedPath ||
  1784.                 $storedVideo === ltrim($videoPath'/')) {
  1785.                 $videoIndex $index;
  1786.                 $videoPath $storedVideo;
  1787.                 break;
  1788.             }
  1789.         }
  1790.         
  1791.         if ($videoIndex === false) {
  1792.             return new JsonResponse(['success' => false'message' => 'Vidéo non trouvée dans la liste'], 404);
  1793.         }
  1794.         unset($videos[$videoIndex]);
  1795.         $videos array_values($videos);
  1796.         $product->setVideos(empty($videos) ? null $videos);
  1797.         $filePathToDelete $this->getParameter('kernel.project_dir') . '/public' $videoPath;
  1798.         if (!file_exists($filePathToDelete) && $videoPath !== $normalizedPath) {
  1799.             $filePathToDelete $this->getParameter('kernel.project_dir') . '/public' $normalizedPath;
  1800.         }
  1801.         
  1802.         if (file_exists($filePathToDelete)) {
  1803.             @unlink($filePathToDelete);
  1804.         }
  1805.         try {
  1806.             $em->flush();
  1807.             return new JsonResponse([
  1808.                 'success' => true,
  1809.                 'message' => 'Vidéo supprimée avec succès',
  1810.                 'data' => [
  1811.                     'images' => $product->getImages(),
  1812.                     'videos' => $product->getVideos(),
  1813.                     'documents' => $product->getDocuments()
  1814.                 ]
  1815.             ]);
  1816.         } catch (\Throwable $e) {
  1817.             return new JsonResponse(['success' => false'message' => 'Erreur lors de la suppression: ' $e->getMessage()], 500);
  1818.         }
  1819.     }
  1820.     #[Route('/shop/{shopSlug}/products/{id}/document/delete'name'product_document_delete'methods: ['POST'])]
  1821.     public function deleteProductDocument(string $shopSlugint $idRequest $requestEntityManagerInterface $em): JsonResponse
  1822.     {
  1823.         if (!$this->getUser() || !$this->isAuthorized()) {
  1824.             return new JsonResponse(['success' => false'message' => 'Non autorisé'], 401);
  1825.         }
  1826.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]);
  1827.         
  1828.         if (!$shop) {
  1829.             return new JsonResponse(['success' => false'message' => 'Boutique non trouvée'], 404);
  1830.         }
  1831.         if (!$shop->getManager()->contains($this->getUser())) {
  1832.             return new JsonResponse(['success' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  1833.         }
  1834.         $product $em->getRepository(Product::class)->find($id);
  1835.         
  1836.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  1837.             return new JsonResponse(['success' => false'message' => 'Produit non trouvé'], 404);
  1838.         }
  1839.         $documentPath $request->request->get('document');
  1840.         if (!$documentPath) {
  1841.             return new JsonResponse(['success' => false'message' => 'Chemin de document manquant'], 400);
  1842.         }
  1843.         $normalizedPath ltrim($documentPath'/');
  1844.         $normalizedPath '/' $normalizedPath;
  1845.         $documents $product->getDocuments() ?: [];
  1846.         
  1847.         $documentIndex false;
  1848.         foreach ($documents as $index => $storedDocument) {
  1849.             $normalizedStored ltrim($storedDocument'/');
  1850.             $normalizedStored '/' $normalizedStored;
  1851.             
  1852.             if ($normalizedStored === $normalizedPath || 
  1853.                 $storedDocument === $documentPath || 
  1854.                 $storedDocument === $normalizedPath ||
  1855.                 $storedDocument === ltrim($documentPath'/')) {
  1856.                 $documentIndex $index;
  1857.                 $documentPath $storedDocument;
  1858.                 break;
  1859.             }
  1860.         }
  1861.         
  1862.         if ($documentIndex === false) {
  1863.             return new JsonResponse(['success' => false'message' => 'Document non trouvé dans la liste'], 404);
  1864.         }
  1865.         unset($documents[$documentIndex]);
  1866.         $documents array_values($documents);
  1867.         $product->setDocuments(empty($documents) ? null $documents);
  1868.         $filePathToDelete $this->getParameter('kernel.project_dir') . '/public' $documentPath;
  1869.         if (!file_exists($filePathToDelete) && $documentPath !== $normalizedPath) {
  1870.             $filePathToDelete $this->getParameter('kernel.project_dir') . '/public' $normalizedPath;
  1871.         }
  1872.         
  1873.         if (file_exists($filePathToDelete)) {
  1874.             @unlink($filePathToDelete);
  1875.         }
  1876.         try {
  1877.             $em->flush();
  1878.             return new JsonResponse([
  1879.                 'success' => true,
  1880.                 'message' => 'Document supprimé avec succès',
  1881.                 'data' => [
  1882.                     'images' => $product->getImages(),
  1883.                     'videos' => $product->getVideos(),
  1884.                     'documents' => $product->getDocuments()
  1885.                 ]
  1886.             ]);
  1887.         } catch (\Throwable $e) {
  1888.             return new JsonResponse(['success' => false'message' => 'Erreur lors de la suppression: ' $e->getMessage()], 500);
  1889.         }
  1890.     }
  1891.     #[Route('/shop/{slug}/banner-image/delete'name'shop_banner_image_delete'methods: ['POST'])]
  1892.     public function deleteShopBannerImage(string $slugRequest $requestEntityManagerInterface $em): JsonResponse
  1893.     {
  1894.         if (!$this->getUser() || !$this->isAuthorized()) {
  1895.             return new JsonResponse(['success' => false'message' => 'Non autorisé'], 401);
  1896.         }
  1897.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  1898.         
  1899.         if (!$shop) {
  1900.             return new JsonResponse(['success' => false'message' => 'Boutique non trouvée'], 404);
  1901.         }
  1902.         // Vérifier que la boutique appartient au vendeur
  1903.         if (!$shop->getManager()->contains($this->getUser())) {
  1904.             return new JsonResponse(['success' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  1905.         }
  1906.         $imagePath $request->request->get('image');
  1907.         if (!$imagePath) {
  1908.             return new JsonResponse(['success' => false'message' => 'Chemin d\'image manquant'], 400);
  1909.         }
  1910.         $bannerImages $shop->getAllBannerImages();
  1911.         $normalizedPath ltrim($imagePath'/');
  1912.         $imageIndex = -1;
  1913.         
  1914.         foreach ($bannerImages as $index => $bannerImage) {
  1915.             $normalizedBannerImage ltrim($bannerImage'/');
  1916.             if ($bannerImage === $imagePath || $normalizedBannerImage === $normalizedPath || $bannerImage === $normalizedPath || $normalizedBannerImage === $imagePath) {
  1917.                 $imageIndex $index;
  1918.                 break;
  1919.             }
  1920.         }
  1921.         if ($imageIndex === -1) {
  1922.             return new JsonResponse(['success' => false'message' => 'Image non trouvée'], 404);
  1923.         }
  1924.         unset($bannerImages[$imageIndex]);
  1925.         $bannerImages array_values($bannerImages);
  1926.         $shop->setBannerImages(empty($bannerImages) ? [] : $bannerImages);
  1927.         $filePathToDelete $this->getParameter('kernel.project_dir') . '/public/' ltrim($imagePath'/');
  1928.         if (!file_exists($filePathToDelete) && $imagePath !== $normalizedPath) {
  1929.             $filePathToDelete $this->getParameter('kernel.project_dir') . '/public/' $normalizedPath;
  1930.         }
  1931.         
  1932.         if (file_exists($filePathToDelete)) {
  1933.             @unlink($filePathToDelete);
  1934.         }
  1935.         try {
  1936.             $em->flush();
  1937.             return new JsonResponse([
  1938.                 'success' => true,
  1939.                 'message' => 'Image de bannière supprimée avec succès',
  1940.                 'data' => [
  1941.                     'bannerImages' => $shop->getAllBannerImages()
  1942.                 ]
  1943.             ]);
  1944.         } catch (\Throwable $e) {
  1945.             return new JsonResponse(['success' => false'message' => 'Erreur lors de la suppression: ' $e->getMessage()], 500);
  1946.         }
  1947.     }
  1948.     #[Route('/shop/{shopSlug}/products/{id}/delete'name'product_delete'methods: ['POST'])]
  1949.     public function deleteProduct(string $shopSlugint $idEntityManagerInterface $em): JsonResponse
  1950.     {
  1951.         if (!$this->getUser() || !$this->isAuthorized()) {
  1952.             return new JsonResponse(['success' => false'message' => 'Non autorisé'], 401);
  1953.         }
  1954.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]);
  1955.         
  1956.         if (!$shop) {
  1957.             return new JsonResponse(['success' => false'message' => 'Boutique non trouvée'], 404);
  1958.         }
  1959.         // Vérifier que la boutique appartient au vendeur
  1960.         if (!$shop->getManager()->contains($this->getUser())) {
  1961.             return new JsonResponse(['success' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  1962.         }
  1963.         $product $em->getRepository(Product::class)->find($id);
  1964.         
  1965.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  1966.             return new JsonResponse(['success' => false'message' => 'Produit non trouvé'], 404);
  1967.         }
  1968.         // Cacher le produit au lieu de le supprimer
  1969.         try {
  1970.             $product->setIsActive(false);
  1971.             $em->flush();
  1972.             
  1973.             return new JsonResponse([
  1974.                 'success' => true,
  1975.                 'message' => 'Produit supprimé avec succès',
  1976.                 'hidden' => true
  1977.             ]);
  1978.         } catch (\Throwable $e) {
  1979.             return new JsonResponse([
  1980.                 'success' => false
  1981.                 'message' => 'Erreur lors de la suppression: ' $e->getMessage()
  1982.             ], 500);
  1983.         }
  1984.     }
  1985.     #[Route('/check-slug'name'shop_check_slug'methods: ['GET'])]
  1986.     public function checkSlug(Request $requestEntityManagerInterface $em): JsonResponse
  1987.     {
  1988.         $slug $request->query->get('slug');
  1989.         if (!$slug) {
  1990.             return new JsonResponse(['valid' => false'message' => 'Slug manquant'], 400);
  1991.         }
  1992.         $exists $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  1993.         return new JsonResponse([
  1994.             'valid' => $exists false true,
  1995.             'message' => $exists 'Ce slug est déjà utilisé' 'Slug disponible'
  1996.         ]);
  1997.     }
  1998.     #[Route('/pricing'name'pricing')]
  1999.     public function pricingSeller(ShopPlanRepository $shopPlanRepository): Response
  2000.     {
  2001.         if (!$this->getUser()) {
  2002.             return $this->redirectToRoute('ui_app_login');
  2003.         }
  2004.         if (!$this->isAuthorized()) {
  2005.             return $this->render('misc/access_denied.html.twig');
  2006.         }
  2007.         $allPlans $shopPlanRepository->findAll();
  2008.         return $this->render('seller/pricing.html.twig', [
  2009.             'current_menu' => 'pricing',
  2010.             'allPlans' => $allPlans
  2011.         ]);
  2012.     }
  2013.     #[Route('/faq'name'faq')]
  2014.     public function faqSeller(): Response
  2015.     {
  2016.         if (!$this->getUser()) {
  2017.             return $this->redirectToRoute('ui_app_login');
  2018.         }
  2019.         if (!$this->isAuthorized()) {
  2020.             return $this->render('misc/access_denied.html.twig');
  2021.         }
  2022.         return $this->render('seller/faq.html.twig', [
  2023.             'current_menu' => 'faq'
  2024.         ]);
  2025.     }
  2026.     #[Route('/terms'name'terms')]
  2027.     public function terms(): Response
  2028.     {
  2029.         if (!$this->getUser()) {
  2030.             return $this->redirectToRoute('ui_app_login');
  2031.         }
  2032.         if (!$this->isAuthorized()) {
  2033.             return $this->render('misc/access_denied.html.twig');
  2034.         }
  2035.         return $this->render('seller/terms.html.twig', [
  2036.             'current_menu' => 'shop'
  2037.         ]);
  2038.     }
  2039.     #[Route('/privacy'name'privacy')]
  2040.     public function privacy(): Response
  2041.     {
  2042.         if (!$this->getUser()) {
  2043.             return $this->redirectToRoute('ui_app_login');
  2044.         }
  2045.         if (!$this->isAuthorized()) {
  2046.             return $this->render('misc/access_denied.html.twig');
  2047.         }
  2048.         return $this->render('seller/privacy.html.twig', [
  2049.             'current_menu' => 'shop'
  2050.         ]);
  2051.     }
  2052.     #[Route('/shop/{slug}/orders'name'shop_orders')]
  2053.     public function shopOrders(string $slugEntityManagerInterface $em): Response
  2054.     {
  2055.         if (!$this->getUser()) {
  2056.             return $this->redirectToRoute('ui_app_login');
  2057.         }
  2058.         if (!$this->isAuthorized()) {
  2059.             return $this->render('misc/access_denied.html.twig');
  2060.         }
  2061.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  2062.         
  2063.         if (!$shop) {
  2064.             $this->addFlash('error''Boutique non trouvée.');
  2065.             return $this->redirectToRoute('seller_index');
  2066.         }
  2067.         // Vérifier que la boutique appartient à l'utilisateur connecté
  2068.         if (!$shop->getManager()->contains($this->getUser())) {
  2069.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  2070.             return $this->redirectToRoute('seller_index');
  2071.         }
  2072.         // TODO: Implémenter la logique pour afficher les commandes
  2073.         return $this->render('seller/orders.html.twig', [
  2074.             'shop' => $shop,
  2075.             'current_menu' => 'shop'
  2076.         ]);
  2077.     }
  2078.     #[Route('/shop/{slug}/transactions'name'shop_transactions')]
  2079.     public function shopTransactions(string $slugEntityManagerInterface $em): Response
  2080.     {
  2081.         if (!$this->getUser()) {
  2082.             return $this->redirectToRoute('ui_app_login');
  2083.         }
  2084.         if (!$this->isAuthorized()) {
  2085.             return $this->render('misc/access_denied.html.twig');
  2086.         }
  2087.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  2088.         
  2089.         if (!$shop) {
  2090.             $this->addFlash('error''Boutique non trouvée.');
  2091.             return $this->redirectToRoute('seller_index');
  2092.         }
  2093.         // Vérifier que la boutique appartient à l'utilisateur connecté
  2094.         if (!$shop->getManager()->contains($this->getUser())) {
  2095.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  2096.             return $this->redirectToRoute('seller_index');
  2097.         }
  2098.         // TODO: Implémenter la logique pour afficher les transactions
  2099.         return $this->render('seller/transactions.html.twig', [
  2100.             'shop' => $shop,
  2101.             'current_menu' => 'shop'
  2102.         ]);
  2103.     }
  2104.     #[Route('/shop/{slug}/team'name'shop_team')]
  2105.     public function shopTeam(string $slugRequest $requestEntityManagerInterface $emShopLimitService $shopLimitService): Response
  2106.     {
  2107.         if (!$this->getUser()) {
  2108.             return $this->redirectToRoute('ui_app_login');
  2109.         }
  2110.         if (!$this->isAuthorized()) {
  2111.             return $this->render('misc/access_denied.html.twig');
  2112.         }
  2113.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  2114.         
  2115.         if (!$shop) {
  2116.             $this->addFlash('error''Boutique non trouvée.');
  2117.             return $this->redirectToRoute('seller_index');
  2118.         }
  2119.         // Vérifier que la boutique appartient à l'utilisateur connecté
  2120.         if (!$shop->getManager()->contains($this->getUser())) {
  2121.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  2122.             return $this->redirectToRoute('seller_index');
  2123.         }
  2124.         // Gérer l'ajout d'un membre
  2125.         if ($request->isMethod('POST') && $request->request->has('add_member')) {
  2126.             $email trim($request->request->get('email'''));
  2127.             
  2128.             if (empty($email)) {
  2129.                 $this->addFlash('error''L\'email est requis.');
  2130.             } else {
  2131.                 // Vérifier la limite d'employés avec le service
  2132.                 $employeeLimitCheck $shopLimitService->canShopAddEmployee($shop);
  2133.                 if (!$employeeLimitCheck['allowed']) {
  2134.                     $this->addFlash('error'$employeeLimitCheck['message']);
  2135.                     return $this->redirectToRoute('seller_shop_team', ['slug' => $slug]);
  2136.                 }
  2137.                 
  2138.                 // Vérifier aussi que l'utilisateur à ajouter n'a pas atteint sa limite de boutiques
  2139.                 $userRepo $em->getRepository(User::class);
  2140.                 $userToAdd $userRepo->findOneBy(['email' => $email]);
  2141.                 
  2142.                 if (!$userToAdd) {
  2143.                     $this->addFlash('error''Aucun utilisateur trouvé avec cet email.');
  2144.                 } elseif ($shop->getManager()->contains($userToAdd)) {
  2145.                     $this->addFlash('warning''Cet utilisateur fait déjà partie de l\'équipe.');
  2146.                 } else {
  2147.                     // Vérifier que l'utilisateur peut être membre d'une nouvelle boutique
  2148.                     $userShopLimitCheck $shopLimitService->canUserCreateShop($userToAdd);
  2149.                     if (!$userShopLimitCheck['allowed']) {
  2150.                         $this->addFlash('error'sprintf(
  2151.                             'Cet utilisateur a déjà atteint sa limite de %d boutiques. Il ne peut pas être ajouté à une nouvelle boutique.',
  2152.                             $userShopLimitCheck['max_count']
  2153.                         ));
  2154.                     } else {
  2155.                         $shop->addManager($userToAdd);
  2156.                         $shop->setCurrentEmployees($shop->getManager()->count());
  2157.                         $em->flush();
  2158.                         $this->addFlash('success''Membre ajouté à l\'équipe avec succès.');
  2159.                     }
  2160.                 }
  2161.             }
  2162.             
  2163.             return $this->redirectToRoute('seller_shop_team', ['slug' => $slug]);
  2164.         }
  2165.         // Gérer la suppression d'un membre
  2166.         if ($request->isMethod('POST') && $request->request->has('remove_member')) {
  2167.             $memberId = (int) $request->request->get('member_id');
  2168.             $userRepo $em->getRepository(User::class);
  2169.             $member $userRepo->find($memberId);
  2170.             $currentUser $this->getUser();
  2171.             
  2172.             if (!$member) {
  2173.                 $this->addFlash('error''Membre non trouvé.');
  2174.             } elseif ($currentUser instanceof User && $member->getId() === $currentUser->getId()) {
  2175.                 $this->addFlash('error''Vous ne pouvez pas vous retirer vous-même de l\'équipe.');
  2176.             } elseif (!$shop->getManager()->contains($member)) {
  2177.                 $this->addFlash('error''Ce membre ne fait pas partie de l\'équipe.');
  2178.             } else {
  2179.                 $shop->removeManager($member);
  2180.                 $shop->setCurrentEmployees($shop->getManager()->count());
  2181.                 $em->flush();
  2182.                 $this->addFlash('success''Membre retiré de l\'équipe avec succès.');
  2183.             }
  2184.             
  2185.             return $this->redirectToRoute('seller_shop_team', ['slug' => $slug]);
  2186.         }
  2187.         // Récupérer tous les membres de l'équipe
  2188.         $teamMembers $shop->getManager()->toArray();
  2189.         
  2190.         // Récupérer les informations du plan pour l'affichage
  2191.         $plan $shop->getPlan();
  2192.         $maxEmployees $plan $plan->getMaxEmployees() : null;
  2193.         $currentEmployeesCount count($teamMembers);
  2194.         $canAddMore $maxEmployees === null || $currentEmployeesCount $maxEmployees;
  2195.         return $this->render('seller/team.html.twig', [
  2196.             'shop' => $shop,
  2197.             'teamMembers' => $teamMembers,
  2198.             'plan' => $plan,
  2199.             'maxEmployees' => $maxEmployees,
  2200.             'currentEmployeesCount' => $currentEmployeesCount,
  2201.             'canAddMore' => $canAddMore,
  2202.             'current_menu' => 'shop'
  2203.         ]);
  2204.     }
  2205.     private function isAuthorized(): bool
  2206.     {
  2207.         if (!$this->isGranted('IS_AUTHENTICATED_FULLY')) {
  2208.             return false;
  2209.         }
  2210.         $user $this->getUser();
  2211.         return $user && in_array('ROLE_SELLER'$user->getRoles());
  2212.     }
  2213. }