src/Controller/Admin/UserController.php line 605

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Admin;
  3. use App\Doctrine\Type\ClientType;
  4. use App\Entity\Activity;
  5. use App\Entity\Comment;
  6. use App\Entity\Document;
  7. use App\Entity\User;
  8. use App\Form\AdministationChangePwType;
  9. use App\Form\UserType;
  10. use App\Repository\UserRepository;
  11. use App\Repository\SupplierRepository;
  12. use App\Service\GlobalVariables;
  13. use Doctrine\ORM\EntityManagerInterface;
  14. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  15. use Symfony\Component\Form\Form;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\HttpFoundation\Response;
  18. use Symfony\Component\Routing\Annotation\Route;
  19. use Symfony\Component\HttpFoundation\JsonResponse;
  20. use App\Form\UserAddType;
  21. use App\Entity\Link;
  22. use App\Form\FilterUserType;
  23. use App\Repository\AddressRepository;
  24. use App\Entity\Address;
  25. use App\Form\AddressType;
  26. use App\Form\AddressUserType;
  27. use App\Form\AddCodePromotionType;
  28. use App\Repository\DocumentRepository;
  29. use App\Service\RightService;
  30. use App\Form\AdministationType;
  31. use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
  32. use App\Service\ActivityService;
  33. use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
  34. use Symfony\Component\Form\Extension\Core\Type\TimeType;
  35. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  36. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  37. use App\Form\ManageBlockType;
  38. use App\Service\DailyBlockService;
  39. use Psr\Log\LoggerInterface;
  40. use App\Repository\UserPromotionHistoryRepository;
  41. use App\Repository\PromotionRepository;
  42. use App\Entity\UserPromotionHistory;
  43. use Doctrine\DBAL\Connection
  44. use App\Entity\Category;
  45. /**
  46.  * @Route("/user")
  47.  */
  48. class UserController extends AbstractController {
  49.     use AccessTrait;
  50.     private $addressRepository;
  51.     private $userRepository;
  52.     private $documentRepository;
  53.     private $rightService;
  54.     private $activityService;
  55.     public function __construct(
  56.             AddressRepository $addressRepository,
  57.             UserRepository $userRepository,
  58.             SupplierRepository $supplierRepository,
  59.             DocumentRepository $documentRepository,
  60.             RightService $rightService,
  61.             ActivityService $activityService,
  62.             GlobalVariables $globalVariables
  63.     ) {
  64.         $this->addressRepository $addressRepository;
  65.         $this->userRepository $userRepository;
  66.         $this->supplierRepository $supplierRepository;
  67.         $this->documentRepository $documentRepository;
  68.         $this->rightService $rightService;
  69.         $this->activityService$activityService;
  70.         $this->globalVariables $globalVariables;
  71.     }
  72.     
  73.     /**
  74.      * @Route("/show/{id}", name="user_show", methods={"GET","POST"}, options = { "expose" =  true})
  75.      */
  76.     public function show(Request $requestUser $user,EntityManagerInterface $em,DocumentRepository $documentRepository,UserPromotionHistoryRepository $historyRepo): Response {
  77.         $this->hasRight($request,'USERS');
  78.         $form $this->createForm(AddCodePromotionType::class, null);
  79.         $form->handleRequest($request);
  80.         if( $form->isSubmitted() && $form->isValid()) {
  81.             $em->flush();
  82.             return $this->redirect($this->generateUrl('user_show', ['id' => $user->getId()]));
  83.         }
  84.         $cmdEnAttente $user->getDocuments()->filter(function($element) {
  85.             return in_array($element->getStatus(), ['en-attente']) and  $element->getType() =='commande';
  86.         })->count();
  87.          $cmdAccepte $user->getDocuments()->filter(function($element) {
  88.             return in_array($element->getStatus(), ['accepte']) and  $element->getType() =='commande';
  89.         })->count();
  90.         $cmdExpedie $user->getDocuments()->filter(function($element) {
  91.             return in_array($element->getStatus(), ['expedie']) and  $element->getType() =='commande';
  92.         })->count();
  93.         $cmdLivre $user->getDocuments()->filter(function($element) {
  94.             return in_array($element->getStatus(), ['livre']) and  $element->getType() =='commande';
  95.         })->count();
  96.         $cmdAnnule $user->getDocuments()->filter(function($element) {
  97.             return in_array($element->getStatus(), ['annule']) and  $element->getType() =='commande';
  98.         })->count();
  99.         $cmdEnRetour $user->getDocuments()->filter(function($element) {
  100.             return in_array($element->getStatus(), ['retourne','retour-en-cours']) and  $element->getType() =='commande';
  101.         })->count();
  102.         $echangeEnAttente $user->getDocuments()->filter(function($element) {
  103.             return $element->getStatus() =='en-attente' and  $element->getType() =='echange';
  104.         })->count();
  105.         $nbCmdTotal $user->getDocuments()->filter(function($element) {
  106.             return  $element->getType() =='commande';
  107.         })->count();
  108.         $nbBeTotal $user->getDocuments()->filter(function($element) {
  109.             return  $element->getType() =='echange';
  110.         })->count();
  111.         $nbFacTotal $user->getDocuments()->filter(function($element) {
  112.             return  $element->getType() =='facture';
  113.         })->count();
  114.         $totalAchat $user->getDocuments()->filter(function($element) {
  115.             return in_array($element->getStatus(), ['expedie','livre','paye','facture']) && $element->getType() == 'commande';
  116.         
  117.         })->map(function($element) {
  118.             return $element->getTotalAmountTtc() -$element->getDeliveryTotal();
  119.         })->reduce(function($carry$item) {
  120.             return $carry $item;
  121.         }, 0);
  122.         $categoriesStat $this->getDoctrine()->getRepository(Document::class)->countByProductCategory($user);
  123.         $statsParSource $this->getDoctrine()->getRepository(Document::class)->countBySource($user);
  124.         $cmdByYear $this->getDoctrine()->getRepository(Document::class)->countByYear($user);
  125.         $lastCommande $em->getRepository(Document::class)->createQueryBuilder('d')
  126.                             ->select('MAX(d.createdAt)')
  127.                             ->where('d.client = :client')
  128.                             ->andWhere('d.type = :type')
  129.                             ->setParameter('client'$user)
  130.                             ->setParameter('type''commande')
  131.                             ->getQuery()
  132.                             ->getSingleScalarResult();
  133.         $lastCommandeDate $lastCommande ? new \DateTime($lastCommande) : null;
  134.         $daysSinceLastCommande $lastCommandeDate ? (new \DateTime())->diff($lastCommandeDate)->days null;
  135.         $averageDelay $documentRepository->getAverageDelayBetweenOrders($user);
  136.         $averageAOV $documentRepository->getAverageOrderValue($user'ttc');
  137.         
  138.        
  139.         $promo $user->getPromotion();
  140.         $usedCount null;
  141.         if ($promo) { $usedCount $documentRepository->countValidUsesByClient($promo$user); }
  142.         
  143.         $userPromotionHistories $historyRepo->findBy(
  144.             ['user' => $user],
  145.             ['startedAt' => 'DESC']
  146.         );
  147.         //$statsParSource = $documentRepository->countSource($user);
  148.        
  149.         //dump($cmdByYear); die;
  150.         return $this->render('@admin/user/fiche_client.html.twig', [
  151.             'user' => $user,
  152.             'form' => $form->createView(),
  153.             'statistiques' => compact('cmdEnAttente','cmdAccepte','cmdExpedie','cmdLivre','cmdAnnule','cmdEnRetour','echangeEnAttente','nbCmdTotal','nbFacTotal','nbBeTotal','totalAchat'),
  154.             'categoriesStat' => $categoriesStat,
  155.             'statsParSource' => $statsParSource,
  156.             'cmdByYear' => $cmdByYear,
  157.             'lastCommandeDate' => $lastCommandeDate,
  158.             'daysSinceLastCommande' => $daysSinceLastCommande,
  159.             'promoUsedCount'        => $usedCount,
  160.              'userPromotionHistories' => $userPromotionHistories,
  161.              'averageDelay' => $averageDelay,
  162.              'averageAOV' => $averageAOV,
  163.         ]);
  164.     }
  165.     
  166.      /**
  167.      * @Route("/listuser/{type}", name="list_user", methods="GET|POST", options = { "expose" =  true})
  168.      */
  169.     public function listData($typeRequest $requestUserRepository $users) {
  170.         $clientType $request->get('clientType');
  171.     if ($clientType === null) {
  172.         // Log ou message d'erreur
  173.         error_log('clientType non transmis');
  174.     } else {
  175.         error_log('clientType transmis: ' $clientType);
  176.     }
  177.    
  178.     //$clientType = $request->get('form')['clientType'] ?? null;
  179.     //dump($clientType);
  180.    // dump($request->query->all());
  181.         $pagination $request->get('pagination');
  182.         $page $pagination['page'] - 1;
  183.         $limit $pagination['perpage'];
  184.         $dateB null;
  185.        
  186.         if( $request->get('dateBefore')) {
  187.             $dateBefore = new \DateTime($request->get('dateBefore'));
  188.             $dateB $dateBefore->format('Y-m-d h:m:s');
  189.         }
  190.         $dateA null;
  191.         if( $request->get('dateAfter')) {
  192.             $dateAfter = new \DateTime($request->get('dateAfter'));
  193.             $dateA $dateAfter->format('Y-m-d h:m:s');
  194.         }
  195.         $sortField=($request->get('sort')&&$type== "client")?$request->get('sort')["field"]:'createdAt';
  196.         $sortType=$request->get('sort')?$request->get('sort')["sort"]:'DESC';
  197.        
  198.         //$count = $users->countUsers($request->get('username'), $request->get('phone'), $type, $request->get('email'), $dateB, $dateA, $request->get('region'), $request->get('clientType'), $request->get('city'));
  199.         $count $users->countUsers($request->get('username'), $request->get('phone'), $type$request->get('email'),$dateB$dateA$request->get('region'),  $request->get('clientType'),$request->get('city'));
  200.         $output = array(
  201.             'data' => array(),
  202.             'meta' => array(
  203.                 'page' => $pagination['page'],
  204.                 'perpage' => $limit,
  205.                 "pages" => ceil($count $limit),
  206.                 "total" => $count,
  207.             )
  208.         );
  209.         if($type== "client"){
  210.             $entities $users->clientList($page$limit$request->get('username'), $request->get('phone'), $type$request->get('email'), $dateB$dateA$request->get('region'), $request->get('clientType'), $request->get('city'),$sortField,$sortType);
  211.             foreach ($entities as $entity) {
  212.                 $data = [
  213.                     'id' => $entity["customer"]->getId(),
  214.                     'userName' => $entity["customer"]->getUsername(),
  215.                     'firstName' => $entity["customer"]->getCivility() . ' ' $entity["customer"]->getFirstName() ,
  216.                     'phone' => $entity["customer"]->getPhone() == "" 'Pas mentionné' $entity["customer"]->getPhone(),
  217.                     'email' => $entity["customer"]->getEmail() == "" 'Pas mentionné' $entity["customer"]->getEmail(),
  218.                     'adress' => $entity["customer"]->getAdress(),
  219.                     'region' => $entity["customer"]->getRegion(),
  220.                     'clientType' => $entity["customer"]->getClientType(),
  221.                     'city' => $entity["customer"]->getCity(),
  222.                     'description' => $entity["customer"]->getDescription() ?? 'Pas mentionné',                    
  223.                     'createdAt' => $entity["customer"]->getCreatedAt()->format('d/m/Y'),
  224.                     'total_Achat'=>$entity["total_Achat"],
  225.                     'date_last_cmd'=>$this->convertDate($entity["date_last_cmd"]),
  226.                     'nbCmdTotal' => $entity["nbCmdTotal"],
  227.                     'cmdOK' =>  $entity["cmdOK"],
  228.                     'cmdEnRetour' =>  $entity["cmdEnRetour"],
  229.                     'cmdAnnulee' =>  $entity["cmdAnnulee"],
  230.                     'nbEchangeTotal' =>  $entity["nbEchangeTotal"],
  231.                     'actions' => 'actions'
  232.                 ];
  233.                 //dump($entity["customer"]->getClientType());
  234.                 $output['data'][]=$data;
  235.             }
  236.         }else{
  237.             $entities $users->search($page$limit$request->get('username'), $request->get('phone'), $type$request->get('email'), $dateB$dateA$request->get('region'), $request->get('clientType'), $request->get('city'),$sortField,$sortType);
  238.             foreach ($entities as $entity) {
  239.                 $data = [
  240.                     'id' => $entity->getId(),
  241.                     'name' => $entity->getCivility() . ' ' $entity->getFirstName() ,
  242.                     'phone' => $entity->getPhone() == "" 'Pas mentionné' $entity->getPhone(),
  243.                     'email' => $entity->getEmail() == "" 'Pas mentionné' $entity->getEmail(),
  244.                     'description' => $entity->getDescription() ?? 'Pas mentionné',
  245.                     'clientType' => $entity->getClientType(),
  246.                     'createdAt' => $entity->getCreatedAt()->format('d/m/Y'),
  247.                     'actions' => 'actions'
  248.                 ];
  249.                 $output['data'][]=$data;
  250.             }
  251.         }
  252.         return new JsonResponse($output);
  253.     }
  254.     /**
  255.      * @Route("/index/{type}", name="user_index", methods={"GET"})
  256.      */
  257.     public function index($type,Request $request): Response {
  258.        
  259.         $rights $this->rightService->getAllRights($this->getUser());
  260.         if( !in_array('USERS'$rights)) {
  261.             $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  262.             return $this->redirect($this->generateUrl('index'));
  263.         }
  264.         $form $this->createForm(FilterUserType::class, null, ['is_client_filter' => true,]);
  265.         return $this->render('@admin/user/list_users.html.twig', [
  266.                     'form' => $form->createView(),
  267.                     'type' => $type,
  268.                     'rights' => $rights
  269.         ]);
  270.     }
  271.     public function modal(Request $request): Response {
  272.             $user = new User();
  273.             $form $this->createForm(UserAddType::class, $user);
  274.             return $this->render('@admin/user/_formAddUserModal.html.twig', [
  275.                         'user' => $user,
  276.                         'form' => $form->createView(),
  277.             ]);
  278.         /*$request->getSession()->getFlashBag()->add('danger', "Accès refusé");
  279.         return $this->redirect($this->generateUrl('index'));*/
  280.     }
  281.     /**
  282.      * @Route("/new", name="user_new", methods={"GET","POST"})
  283.      */
  284.     public function new(Request $request,EntityManagerInterface $em): Response {
  285.         $rights $this->rightService->getAllRights($this->getUser());
  286.         if( in_array('USERS_CREATE'$rights)) {
  287.             $user = new User();
  288.             $form $this->createForm(UserType::class, $user);
  289.             $form->handleRequest($request);
  290.             if( $form->isSubmitted() && $form->isValid()) {
  291.                 $em->persist($user);
  292.                 $em->flush();
  293.                 return $this->redirectToRoute('user_index');
  294.             }
  295.             return $this->render('@admin/user/new.html.twig', [
  296.                         'user' => $user,
  297.                         'form' => $form->createView(),
  298.                         'rights' => $rights
  299.             ]);
  300.         } else {
  301.             $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  302.             return $this->redirect($this->generateUrl('index'));
  303.         }
  304.     }
  305.      /**
  306.      * @Route("/add", name="add_user", methods={"GET","POST"}, options={"expose"=true})
  307.      */
  308.     public function add(Request $requestUserRepository $usersEntityManagerInterface $em): JsonResponse
  309.     {
  310.         $rights $this->rightService->getAllRights($this->getUser());
  311.         if (!in_array('USERS_CREATE'$rights)) {
  312.             $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  313.             // Pour un appel AJAX, on peut aussi renvoyer un JSON d'erreur si besoin
  314.             return new JsonResponse([
  315.                 'success' => false,
  316.                 'message' => "Accès refusé.",
  317.             ]);
  318.         }
  319.         $type     $request->get('type');
  320.         $civility $request->get('civility');
  321.         $username trim((string) $request->get('username'));
  322.         $phone    trim((string) $request->get('phone'));
  323.         // 1) Validation des champs obligatoires
  324.         if (!$civility || !$username || !$phone) {
  325.             $message 'Veuillez renseigner tous les champs obligatoires : Civilité, Référence et Téléphone.';
  326.             if (!$civility) {
  327.                 $message 'La civilité est obligatoire.';
  328.             } elseif (!$username) {
  329.                 $message 'La référence est obligatoire.';
  330.             } elseif (!$phone) {
  331.                 $message 'Le téléphone est obligatoire.';
  332.             }
  333.             return new JsonResponse([
  334.                 'success' => false,
  335.                 'message' => $message,
  336.             ]);
  337.         }
  338.         // 2) Unicité téléphone / référence
  339.         $existUsername $users->findOneBy(['username' => $username]);
  340.         $existPhone    $users->findOneBy(['phone' => $phone]);
  341.         if ($existPhone) {
  342.             return new JsonResponse([
  343.                 'success' => false,
  344.                 'message' => "Téléphone que vous avez entré existe déjà.",
  345.             ]);
  346.         }
  347.         if ($existUsername) {
  348.             return new JsonResponse([
  349.                 'success' => false,
  350.                 'message' => "Référence que vous avez entré existe déjà.",
  351.             ]);
  352.         }
  353.         // 3) Création du User
  354.         $user $this->createUser($request);
  355.         $em->persist($user);
  356.         // 4) Liens réseaux sociaux (structure existante)
  357.         $linksData $request->get('links', []);
  358.         foreach ($linksData as $linkData) {
  359.             if (!empty($linkData['link'])) {
  360.                 $link = new Link();
  361.                 $link->setIcon($linkData['icon'] ?? '')
  362.                     ->setLink($linkData['link'])
  363.                     ->setUser($user);
  364.                 $em->persist($link);
  365.             }
  366.         }
  367.         $em->flush();
  368.         // 5) Flashs de confirmation (en plus du JSON)
  369.         if ($type === 'client') {
  370.             $request->getSession()->getFlashBag()->add('success'"Client ajouté avec succès");
  371.         } elseif ($type === 'prospect') {
  372.             $request->getSession()->getFlashBag()->add('success'"Prospect ajouté avec succès");
  373.         } elseif ($type === 'contact') {
  374.             $request->getSession()->getFlashBag()->add('success'"Contact ajouté avec succès");
  375.         }
  376.         // 6) Redirection côté JS
  377.         if ($request->get('save') === 'save') {
  378.             return new JsonResponse([
  379.                 'success' => true,
  380.                 'path'    => $this->generateUrl('user_show', ['id' => $user->getId()]),
  381.             ]);
  382.         }
  383.         return new JsonResponse([
  384.             'success' => true,
  385.             'path'    => $this->generateUrl('new_document_commande', [
  386.                 'category' => 'client',
  387.                 'id'       => $user->getId(),
  388.             ]),
  389.         ]);
  390.     }
  391.      /**
  392.      * @Route("/delete/{id}", name="user_delete", methods={"GET","DELETE"}, options={"expose"=true})
  393.      */        
  394.     public function delete_user(Request $requestUser $userEntityManagerInterface $em): JsonResponse
  395.     {
  396.         $this->hasRight($request'USERS_DELETE');
  397.         // Vérifie si le client a des documents de type "commande"
  398.         $hasCommandes $user->getDocuments()->exists(function($key$doc) {
  399.             return $doc->getType() === 'commande';
  400.         });
  401.         if ($hasCommandes) {
  402.             return new JsonResponse([
  403.                 'success' => false,
  404.                 'message' => "Impossible de supprimer ce client : il possède encore des commandes. Veuillez d'abord annuler et supprimer toutes ses commandes."
  405.             ], Response::HTTP_BAD_REQUEST);
  406.         }
  407.         // Suppression du client (aucune commande liée)
  408.         $em->remove($user);
  409.         $em->flush();
  410.         return new JsonResponse([
  411.             'success' => true,
  412.             'message' => "Client supprimé avec succès."
  413.         ]);
  414.     }
  415.     /**
  416.      * @Route("/utilisateur/{id}", name="user_profile", methods={"GET"}, options = { "expose" =  true})
  417.      */
  418.     public function userProfile(User $user): Response
  419.     {
  420.         $this->denyAccessUnlessGranted('ROLE_ADMIN'); // ou ta méthode hasRight
  421.         return $this->render('@admin/user/fiche_user.html.twig', [
  422.             'user' => $user,
  423.         ]);
  424.     }
  425.     /**
  426.      * @Route("/edit/{id}", name="user_edit", methods={"GET","POST"}, options={"expose"=true})
  427.      */
  428.     /*
  429.     public function edit(Request $request, User $user,EntityManagerInterface $em): Response {
  430.         $this->hasRight($request,'USERS_UPDATE');
  431.         $form = $this->createForm(UserType::class, $user);
  432.         $form->handleRequest($request);
  433.         if( $form->isSubmitted() && $form->isValid()) {
  434.             $em->flush();
  435.             return $this->redirectToRoute('user_index');
  436.         }
  437.         return $this->render('@admin/user/edit.html.twig', [
  438.                     'user' => $user,
  439.                     'form' => $form->createView(),
  440.         ]);
  441.     }
  442.     */
  443.     public function createUser($request) {
  444.         $user = new User();
  445.         $user->setCivility($request->get('civility'))
  446.                 ->setAdress($request->get('address'))
  447.                 ->setCity($request->get('city'))
  448.                 ->setCountry($request->get('country'))
  449.                 ->setFirstName($request->get('firstName'))
  450.                 ->setPhone(trim($request->get('phone')))
  451.                 //->setRegion($request->get('region'))
  452.                 ->setRegion($request->request->get('region') ?? '')
  453.                 ->setSecondPhone($request->get('second_phone'))
  454.                 ->setType($request->get('type'))
  455.                 ->setUsername($request->get('username'))
  456.                 ->setClientType(ClientType::TYPE_NOUVEAU_CLIENT)
  457.                 ->setZip($request->get('zip'))
  458.                 ->setCreatedAt(new \DateTime('now'));
  459.         return $user;
  460.     }
  461.    
  462.     /**
  463.      * @Route("/search", name="search_client", methods="GET|POST", options = { "expose" =  true})
  464.      */
  465.     public function searchItem(Request $requestUserRepository $users) {
  466.         $query $request->get('query');
  467.         $deleteEspace str_replace(' '''$query);
  468.         if( is_numeric($deleteEspace)) $query $deleteEspace;
  469.         $entities $users->search(010$querynull'client');
  470.         foreach ($entities as $entity) {
  471.            // dd($entity->getDocuments()->first());
  472.             $output[] = [
  473.                 'id' => $entity->getId(),
  474.                 'username' => $entity->getUsername(),
  475.                 'name' => $entity->getCivility() . ' ' $entity->getFirstName() ,
  476.                 'phone' => $entity->getPhone(),
  477.                 'email' => $entity->getEmail(),
  478.                 'address' => $entity->getCity().','.$entity->getRegion(),
  479.                 'cmdEnAttente' => $entity->getDocuments()->filter(function($element) {
  480.                     return in_array($element->getStatus(), ['en-attente']) and  $element->getType() =='commande';
  481.                 })->count(),
  482.                 'cmdAccepte' => $entity->getDocuments()->filter(function($element) {
  483.                     return in_array($element->getStatus(), ['accepte','']) and  $element->getType() =='commande';
  484.                 })->count(),
  485.                 'cmdAnnule' => $entity->getDocuments()->filter(function($element) {
  486.                     return in_array($element->getStatus(), ['annule']) and  $element->getType() =='commande';
  487.                 })->count(),
  488.                 'cmdEnRetour' => $entity->getDocuments()->filter(function($element) {
  489.                     return in_array($element->getStatus(), ['retourne','retour-en-cours']) and  $element->getType() =='commande';
  490.                 })->count(),
  491.                 'echangeEnAttente' => $entity->getDocuments()->filter(function($element) {
  492.                     return $element->getStatus() =='en-attente' and  $element->getType() =='echange';
  493.                 })->count(),
  494.                 'nbCmdTotal' => $entity->getDocuments()->filter(function($element) {
  495.                     return  $element->getType() =='commande';
  496.                 })->count(),
  497.                 'nbEchangeTotal' => $entity->getDocuments()->filter(function($element) {
  498.                     return  $element->getType() =='echange';
  499.                 })->count(),
  500.                 'created_at' => $entity->getCreatedAt()->format('d/m/Y H:i'),
  501.                 'actions' => 'actions',
  502.             ];
  503.         }
  504.         if( !$entities) {
  505.             $output = [];
  506.         }
  507.         return new JsonResponse($output);
  508.     }
  509.     /**
  510.      * @Route("/search2", name="search_client2", methods="GET|POST", options = { "expose" =  true})
  511.      */
  512.     public function searchItem2(Request $requestUserRepository $users) {
  513.         $query $request->get('query');
  514.         $deleteEspace str_replace(' '''$query);
  515.         if( is_numeric($deleteEspace))
  516.             $query $deleteEspace;
  517.         $entities $users->search(010$querynull'client');
  518.         $output='<div class="m-list-search__results">';
  519.         foreach ($entities as $entity) {
  520.             $output .= '<a href="/admin/user/show/'.$entity->getId().'" class="m-list-search__result-item">
  521.                             <span class="m-list-search__result-item-pic">
  522.                                 <img class="m--img-rounded" src="https://ui-avatars.com/api/?name=' $entity->getFirstName() .'" title="">
  523.                             </span>
  524.                             <span class="m-list-search__result-item-text">
  525.                                 '.$entity->getCivility() . ' ' $entity->getFirstName() .'
  526.                                 <span class="float-right">'.$entity->getPhone().'</span>
  527.                             </span>
  528.                         </a>';
  529.         }
  530.         $output .='</div>';
  531.         if( !$entities)
  532.             $output ="";
  533.         return new Response($output);
  534.     }
  535.     /**
  536.      * @Route("/address/{type}/{id}", name="modal_addres_user", methods={"GET","POST"}, options={"expose"=true})
  537.      */
  538.     public function modalAdressClient(Request $request$type$id null,EntityManagerInterface $em): Response {
  539.         //todo ; update hasRight in trait to accepte multi value.
  540.         $rights $this->rightService->getAllRights($this->getUser());
  541.         if( !in_array('USERS_UPDATE'$rights) || !in_array('USERS_CREATE'$rights)) {
  542.             $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  543.             return $this->redirect($this->generateUrl('index'));
  544.         }
  545.         if( $type == 'new') {
  546.             $address = new Address();
  547.             $client $this->userRepository->find($id);
  548.             $form $this->createForm(AddressType::class, $address);
  549.         } else if( $type == 'principal') {
  550.             $client $this->userRepository->find($id);
  551.             $form $this->createForm(AddressUserType::class, $client);
  552.         } else {
  553.             $address $this->addressRepository->find($id);
  554.             $client $address->getUser();
  555.             $form $this->createForm(AddressType::class, $address);
  556.         }
  557.         $form->handleRequest($request);
  558.         //dump($client,$type,$id);die;
  559.         if( $form->isSubmitted() && $request->isMethod('POST')) {
  560.             if( $type == 'new') {
  561.                 $address->setCountry($request->get('address')['country'])
  562.                         //->setRegion($request->get('address')['region'])
  563.                         ->setRegion($request->request->get('address')['region'] ?? '')
  564.                         ->setUser($client);
  565.                 $em->persist($address);
  566.             } else if( $type == 'principal')
  567.                 $client->setCountry($request->get('address_user')['country'])
  568.                         ->setRegion($request->get('address_user')['region']);
  569.             else
  570.                 $address->setCountry($request->get('address')['country'])
  571.                         ->setRegion($request->get('address')['region']);
  572.             $em->flush();
  573.             $request->getSession()->getFlashBag()->add('success'"Adresse modifié avec succès");
  574.             return $this->redirect($this->generateUrl('user_show', ['id' => $client->getId()]));
  575.         }
  576.         return $this->render('@admin/includes/modals/_formAddressModal.html.twig', [
  577.                     'form' => $form->createView(),
  578.                     'idAddress' => $id,
  579.                     'type' => $type,
  580.                     'categorie' => 'client'
  581.         ]);
  582.     }
  583.    
  584.     /**
  585.      * @Route("/supplier/address/modal/{id}", name="modal_address_supplier", methods={"GET", "POST"}, options={"expose"=true})
  586.      */
  587.     public function modalAddressSupplier($idRequest $requestEntityManagerInterface $em): Response
  588.     {
  589.         $rights $this->rightService->getAllRights($this->getUser());
  590.         if (!in_array('USERS_UPDATE'$rights) || !in_array('USERS_CREATE'$rights)) {
  591.             $request->getSession()->getFlashBag()->add('danger'"Accès refusé");
  592.             return $this->redirect($this->generateUrl('index'));
  593.         }
  594.         $type $request->get('type'); // <-- à ne pas oublier
  595.         $supplier null;
  596.         if ($type == 'new') {
  597.             $address = new Address();
  598.             $supplier $this->supplierRepository->find($id);
  599.             $form $this->createForm(AddressType::class, $address);
  600.         } elseif ($type == 'principal') {
  601.             $supplier $this->supplierRepository->find($id);
  602.             $form $this->createForm(AddressUserType::class, $supplier);
  603.         } else {
  604.             $address $this->addressRepository->find($id);
  605.             $supplier $address $address->getSupplier() : null;
  606.             $form $this->createForm(AddressType::class, $address);
  607.         }
  608.         // dump($supplier,$type,$id);die;
  609.         $form->handleRequest($request);
  610.         if ($form->isSubmitted() && $request->isMethod('POST')) {
  611.             if ($type == 'new') {
  612.                 $address->setCountry($request->get('address')['country'] ?? '')
  613.                         ->setRegion($request->request->get('address')['region'] ?? '')
  614.                         ->setSupplier($supplier);
  615.                 $em->persist($address);
  616.             } elseif ($type == 'principal') {
  617.                 $supplier->setCountry($request->get('address_user')['country'] ?? '')
  618.                         ->setRegion($request->get('address_user')['region'] ?? '');
  619.             } else {
  620.                 $address->setCountry($request->get('address')['country'] ?? '')
  621.                         ->setRegion($request->get('address')['region'] ?? '');
  622.             }
  623.             $em->flush();
  624.             $request->getSession()->getFlashBag()->add('success'"Adresse modifiée avec succès");
  625.            
  626.             return $this->redirect($this->generateUrl('supplier_show', ['id' => $supplier->getId()]));
  627.            
  628.         }
  629.         return $this->render('@admin/includes/modals/_formAddressModal.html.twig', [
  630.             'form' => $form->createView(),
  631.             'idAddress' => $id,
  632.             'type' => $type,
  633.             'categorie' => 'fournisseur'
  634.         ]);
  635.     }
  636.     /**
  637.      * @Route("/modal/edit/{id}/{document}", name="modal_edit_user", methods={"GET","POST"}, options={"expose"=true}, defaults={"document"=NULL})
  638.      */
  639.     public function modalEditUser(Request $requestUser $user,?Document $document,EntityManagerInterface $em): Response {
  640.         $this->hasRight($request,'USERS_UPDATE');
  641.         $form $this->createForm(UserType::class, $user);
  642.         $form->handleRequest($request);
  643.         //$entityDocument = ($document)?$this->documentRepository->find($document): null;
  644.         if( $form->isSubmitted()) {
  645.             $user->setCountry($request->request->get('user')['country']);
  646.             $user->setRegion($request->request->get('user')['region']);
  647.            // Nettoyer les liens vides
  648.             foreach ($user->getLinks() as $link) {
  649.                 if (empty($link->getLink()) && empty($link->getIcon())) {
  650.                     $user->removeLink($link);
  651.                     $em->remove($link);
  652.                 }
  653.             }
  654.             if( $document) {
  655.                 $document->setAdress($user->getAdress() . ' ' $user->getCity() . ' ' $user->getRegion() . ' ' $user->getCountry());
  656.                 if( $document->getDocument())
  657.                     $document->getDocument()->setAdress($user->getAdress() . ' ' $user->getCity() . ' ' $user->getRegion() . ' ' $user->getCountry());
  658.             }
  659.             $em->flush();
  660.             if( $document)
  661.                 return $this->redirect($this->generateUrl('document_show', ['id' => $document->getId()]));
  662.             else
  663.                 return $this->redirect($this->generateUrl('user_show', ['id' => $user->getId()]));
  664.         }
  665.         if( $document) {
  666.             return $this->render('@admin/user/_formEditUserModal.html.twig', [
  667.                         'form' => $form->createView(),
  668.                         'user' => $user,
  669.                         'document' => $document
  670.             ]);
  671.         } else {
  672.             return $this->render('@admin/user/_formEditUserModal.html.twig', [
  673.                         'form' => $form->createView(),
  674.                         'user' => $user
  675.             ]);
  676.         }
  677.     }
  678.   /**
  679.  * @Route("/address/delete/{id}", name="delete_user_address", methods={"POST"})
  680.  */
  681. public function deleteUserAddress(AddressRepository $addressRepositoryEntityManagerInterface $emint $id): JsonResponse
  682. {
  683.     $address $addressRepository->find($id);
  684.     if (!$address) {
  685.         return new JsonResponse(['success' => false'message' => 'Adresse introuvable.'], 404);
  686.     }
  687.     $em->remove($address);
  688.     $em->flush();
  689.     return new JsonResponse(['success' => true]);
  690. }
  691.     /**
  692.      * @Route("/administration/index", name="administration_index", methods={"GET"})
  693.      */
  694.     public function indexAdministration(Request $request): Response {
  695.         // Vérification du rôle
  696.         if (!$this->isGranted('ROLE_SUPER_ADMIN')) {
  697.             $this->addFlash('danger'"Vous n'avez pas le droit d'accéder à la gestion des utilisateurs.");
  698.             return $this->redirectToRoute('index'); // Redirection vers la page d'accueil admin
  699.         }
  700.         $rights=$this->hasRight($request,'ADMINISTRATION');
  701.         $form $this->createForm(FilterUserType::class, null, ['is_client_filter' => false, ]);
  702.         return $this->render('@admin/user/administrationIndex.html.twig', [
  703.                     'form' => $form->createView(),
  704.                     'rights' => $rights
  705.         ]);
  706.     }
  707.     /**
  708.      * @Route("/listadministration", name="list_administration", methods="GET|POST", options={"expose"=true})
  709.      */
  710.     public function listAdministrationData(Request $requestUserRepository $users) {
  711.         $pagination $request->get('pagination');
  712.         $page $pagination['page'] - 1;
  713.         $limit $pagination['perpage'];
  714.         $dateB $request->get('dateBefore') ? (new \DateTime($request->get('dateBefore')))->format('d/m/Y H:i:s') : null;
  715.         $dateA $request->get('dateAfter') ? (new \DateTime($request->get('dateAfter')))->format('d/m/Y H:i:s') : null;
  716.         $entities $users->search($page$limit
  717.             $request->get('username'),
  718.             $request->get('phone'),
  719.             'user',
  720.             $request->get('email'),
  721.             $dateB,
  722.             $dateA,
  723.             $request->get('region')
  724.         );
  725.         $count $users->countUsers(
  726.             $request->get('username'),
  727.             $request->get('phone'),
  728.             'user',
  729.             $request->get('email'),
  730.             $dateB,
  731.             $dateA,
  732.             $request->get('region')
  733.         );
  734.         $output = [
  735.             'data' => [],
  736.             'meta' => [
  737.                 'page' => $pagination['page'],
  738.                 'perpage' => $limit,
  739.                 'pages' => ceil($count $limit),
  740.                 'total' => $count,
  741.             ]
  742.         ];
  743.         foreach ($entities as $entity) {
  744.             $output['data'][] = [
  745.                 'id' => $entity->getId(),
  746.                 'username' => $entity->getUserName(),
  747.                 'name' => trim($entity->getFirstName()),
  748.                 'email' => $entity->getEmail(),
  749.                 'phone' => $entity->getPhone() ?? '',
  750.                 'group' => $entity->getGroupUser() ? $entity->getGroupUser()->getName() : null,
  751.                 'poste' => $entity->getUserPost() ? $entity->getUserPost()->getName() : null,
  752.                 'roles' => $entity->getRoles(), // tableau JSON natif
  753.                 'isBlocked' => $entity->isBlocked() ? 0,
  754.                 'createdAt' => $entity->getCreatedAt() 
  755.                     ? $entity->getCreatedAt()->format('Y-m-d H:i:s'
  756.                     : null,
  757.             ];
  758.         }
  759.         return new JsonResponse($output);
  760.     }
  761.     /**
  762.      * @Route("/administration/manage-block/{id}", name="modal_manage_block", methods={"GET","POST"}, options={"expose"=true})
  763.      */
  764.     public function modalManageBlock(Request $requestUser $userEntityManagerInterface $emDailyBlockService $svc): Response
  765.     {
  766.         $form $this->createForm(\App\Form\ManageBlockType::class, $user, ['method' => 'POST']);
  767.         if ($request->isMethod('POST')) {
  768.             $form->handleRequest($request);
  769.             if ($form->isSubmitted() && $form->isValid()) {
  770.                 // Récupère le format UI: daily[mon][enabled|start1|end1|start2|end2]
  771.                 $dailyRaw $request->request->get('daily', []);
  772.                 // ✅ Convertit UI -> format normalisé (mon..sun => [ {start,end}, ... ])
  773.                 $normalized $svc->fromUiDaily(is_array($dailyRaw) ? $dailyRaw : []);
  774.                 // (option) si blocage permanent, on peut forcer l'effacement du quotidien
  775.                 // if ($user->isBlocked()) { $normalized = ['mon'=>[], 'tue'=>[], 'wed'=>[], 'thu'=>[], 'fri'=>[], 'sat'=>[], 'sun'=>[]]; }
  776.                 // Sauvegarde: choisis UNE propriété (JSON string ou array JSON)
  777.                 if (method_exists($user'setDailyBlocks')) {
  778.                     $user->setDailyBlocks($svc->toJson($normalized));   // string JSON
  779.                 } elseif (method_exists($user'setBlockedHours')) {
  780.                     $user->setBlockedHours($normalized);                // array (colonne JSON)
  781.                 }
  782.                 $em->flush();
  783.                 if ($request->isXmlHttpRequest()) {
  784.                     return $this->json(['status' => 'ok']);
  785.                 }
  786.                 $this->addFlash('success''Blocage mis à jour avec succès.');
  787.                 return $this->redirectToRoute('administration_index');
  788.             }
  789.             // form invalide → renvoyer le fragment
  790.             return $this->render('@admin/user/_formManageBlockModal.html.twig', [
  791.                 'form'      => $form->createView(),
  792.                 'user'      => $user,
  793.                 'dailyJson' => $svc->toJson($svc->buildUserDailyBlocks($user)),
  794.             ]);
  795.         }
  796.         // GET (ou re-affichage initial)
  797.         $userDailyBlocks $svc->buildUserDailyBlocks($user);
  798.         return $this->render('@admin/user/_formManageBlockModal.html.twig', [
  799.             'form'      => $form->createView(),
  800.             'user'      => $user,
  801.             'dailyJson' => $svc->toJson($userDailyBlocks),
  802.         ]);
  803.     }
  804.     /**
  805.      * @Route("/administration/new", name="modal_new_administration", methods={"GET","POST"}, options={"expose"=true})
  806.      */
  807.     public function modalNewAdministration(Request $requestUserPasswordEncoderInterface $passwordEncoder,EntityManagerInterface $em): Response {
  808.         $this->hasRight($request,'ADMINISTRATION');
  809.         $user = new User();
  810.         $form $this->createForm(AdministationType::class, $user, ['is_edit' => false]);
  811.         $form->handleRequest($request);
  812.         if( $form->isSubmitted() && $request->isMethod('POST')) {
  813.             $user->setCreatedAt(new \DateTime('now'));
  814.             // encode the plain password
  815.             $user->setPassword(
  816.                     $passwordEncoder->encodePassword(
  817.                             $user,
  818.                             $form->get('password')->getData()
  819.                     )
  820.             );
  821.             $user->setRoles(["ROLE_ADMIN"]);
  822.             $user->setType('user');
  823.             $em->persist($user);
  824.             $em->flush();
  825.             return $this->redirect($this->generateUrl('administration_index'));
  826.         }
  827.         return $this->render('@admin/user/_formNewAdministrationModal.html.twig', [
  828.                     'form' => $form->createView(),
  829.                     'user' => $user
  830.         ]);
  831.     }
  832.     /**
  833.      * @Route("/administration/edit/{id}", name="modal_edit_administration", methods={"GET","POST"}, options={"expose"=true})
  834.      */
  835.     public function modalEditAdministration(Request $requestUser $userUserPasswordEncoderInterface $passwordEncoderEntityManagerInterface $em ): Response {
  836.         $this->hasRight($request,'ADMINISTRATION');
  837.         $form $this->createForm(AdministationType::class, $user, ['is_edit' => true]);
  838.         $form->handleRequest($request);
  839.         if ($form->isSubmitted() && $form->isValid()) {   
  840.             $em->flush();
  841.             return $this->redirectToRoute('administration_index');
  842.         }
  843.         
  844.         return $this->render('@admin/user/_formEditAdministrationModal.html.twig', [
  845.             'form' => $form->createView(),
  846.             'user' => $user
  847.         ]);
  848.     }
  849.     
  850.     /**
  851.      * @Route("/administration/update_password/{id}", name="modal_update_pw_administration", methods={"GET","POST"}, options={"expose"=true})
  852.      */
  853.     public function modalEditPwAdministration(Request $requestUser $userUserPasswordEncoderInterface $passwordEncoder,EntityManagerInterface $em): Response {
  854.         $this->hasRight($request,'ADMINISTRATION');
  855.         $form $this->createForm(AdministationChangePwType::class, $user);
  856.         if( $request->isMethod('POST'))
  857.         {
  858.             $form->handleRequest($request);
  859.             if( !$form->isValid()) {//&& $request->isMethod('POST')
  860.                 return new JsonResponse(array(
  861.                     'result' => 0,
  862.                     'message' => 'Invalid form',
  863.                     'data' => $this->getErrorMessages($form)
  864.                 ));
  865.             }else{
  866.                 $user->setPassword(
  867.                     $passwordEncoder->encodePassword(
  868.                         $user,
  869.                         $form->get('password')->getData()
  870.                     )
  871.                 );
  872.                 $em->persist($user);
  873.                 $em->flush();
  874.                 $this->activityService->addActivity('warning',$this->getUser()->getUserIdentifier(). " a changé le mot de passe de l'utilisateur ".$user->getUserIdentifier(), null$this->getUser(), 'user');
  875.                 return new JsonResponse(array(
  876.                     'result' => 1,
  877.                     'message' => 'Le mot de passe de '.$user->getUsername().' a été mis à jour avec succès',
  878.                     'data' => ''));
  879.             }
  880.         }
  881.         return $this->render('@admin/user/_formEditPwModal.html.twig', [
  882.             'form' => $form->createView(),
  883.             'user' => $user
  884.         ]);
  885.     }
  886.     public function recentDangerActivities(Request $request,EntityManagerInterface $em): Response
  887.     {
  888.         $repository $em->getRepository(Activity::class);
  889.         //dump($repository->findBy(['type' => 'danger']));die;
  890.         return $this->render('@admin/includes/_topnavDangerActivities.html.twig',[
  891.             'activities' => $repository->findBy(['type' => 'danger'],['createdAt'=>'DESC'])
  892.         ]);
  893.     }
  894.     // Generate an array contains a key -> value with the errors where the key is the name of the form field
  895.     protected function getErrorMessages(Form $form)
  896.     {
  897.         $errors = array();
  898.         foreach ($form->getErrors() as $key => $error) {
  899.             $errors[] = $error->getMessage();
  900.         }
  901.         foreach ($form->all() as $child) {
  902.             if( !$child->isValid())
  903.                 $errors[$child->getName()] = $this->getErrorMessages($child);
  904.         }
  905.         return $errors;
  906.     }
  907.     /**
  908.      * @Route("/new_comment/{id}", name="client_comment_new", methods={"GET","POST"})
  909.      */
  910.     public function newComment(Request $request$id,EntityManagerInterface $em): Response {
  911.         $comment = new Comment();
  912.         if( $request->isMethod('POST') && $request->get('form_comment')) {
  913.            // dd($request);
  914.             $comment->setDescription($request->get('form_comment')['comment']);
  915.             $comment->setUser($this->getUser());
  916.             $comment->setCreateAt(new \DateTime('now'));
  917.             $client $this->userRepository->find($id);
  918.             $comment->setClient($client);
  919.             $em->persist($comment);
  920.             $em->flush();
  921.             return $this->redirectToRoute("user_show", ['id' => $id,"tab"=>"comments"]);
  922.         }
  923.         return false;
  924.     }
  925.     /**
  926.      * @Route("/edit_comment/{id}/{comment}", name="client_comment_edit", methods={"GET","POST"})
  927.      */
  928.     public function editComment(Request $request$idComment $comment,EntityManagerInterface $em): Response {
  929.         if( $request->isMethod('POST') && $request->get('form_comment')) {
  930.             $comment->setDescription($request->get('form_comment')['comment']);
  931.             $comment->setUser($this->getUser());
  932.             $comment->setCreateAt(new \DateTime('now'));
  933.             $em->flush();
  934.             return $this->redirectToRoute("user_show", ['id' => $id,"tab"=>"comments"]);
  935.         }
  936.         return  false;
  937.     }
  938.     
  939.     private function convertDate($originalDate)
  940.     {
  941.         $date \DateTime::createFromFormat('Y-m-d H:i:s'$originalDate);
  942.         if ($date === false) {
  943.            return ("-");
  944.         }
  945.         $formattedDate $date->format('d/m/Y');
  946.         return  $formattedDate;
  947.     }
  948.     /**
  949.      * @Route("/admin/user/{id}/promotion", name="user_apply_promotion", methods={"POST"}, options={"expose"=true})
  950.      */
  951.     public function applyClientPromo(Request $requestUser $userEntityManagerInterface $emUserPromotionHistoryRepository $historyRepo): Response {
  952.         //$em->refresh($user);
  953.         $tab $request->request->get('tab''promotion');
  954.         $now = new \DateTimeImmutable();
  955.         $operator $this->getUser()?->getUserIdentifier() ?? $this->getUser()->getFirstName();
  956.         // {# #} Recréer le même formulaire que dans show()
  957.         $form $this->createForm(AddCodePromotionType::class);
  958.         $form->handleRequest($request);
  959.          //dump($user->getPromotion()); die;
  960.         if (!$form->isSubmitted()) {
  961.             $this->addFlash('danger''Formulaire non soumis.');
  962.             return $this->redirectToRoute('user_show', [
  963.                 'id'  => $user->getId(),
  964.                 'tab' => $tab,
  965.             ]);
  966.         }
  967.         /** @var Promotion|null $promotion */
  968.         $promotion $form->get('promotion')->getData();
  969.         // {# #} Aucun code choisi
  970.         if (!$promotion) {
  971.             $this->addFlash('warning''Veuillez choisir un code promotionnel.');
  972.             return $this->redirectToRoute('user_show', [
  973.                 'id'  => $user->getId(),
  974.                 'tab' => $tab,
  975.             ]);
  976.         }
  977.         // {# #} Vérifier type client
  978.         if ($promotion->getType() !== 'client') {
  979.             $this->addFlash('danger''Ce code promotionnel n’est pas destiné aux clients.');
  980.             return $this->redirectToRoute('user_show', [
  981.                 'id'  => $user->getId(),
  982.                 'tab' => $tab,
  983.             ]);
  984.         }
  985.         // {# #} Vérifier période
  986.         if ($promotion->getStartAt() > $now || $promotion->getEndAt() < $now) {
  987.             $this->addFlash('danger''Ce code n’est pas valable actuellement.');
  988.             return $this->redirectToRoute('user_show', [
  989.                 'id'  => $user->getId(),
  990.                 'tab' => $tab,
  991.             ]);
  992.         }
  993.         // {# #} Vérifier quantité
  994.         if ($promotion->getTotalQuantity() !== null && $promotion->getQuantityUser() >= $promotion->getTotalQuantity()) {
  995.             $this->addFlash('danger''Ce code a atteint sa limite d’utilisation.');
  996.             return $this->redirectToRoute('user_show', [
  997.                 'id'  => $user->getId(),
  998.                 'tab' => $tab,
  999.             ]);
  1000.         }
  1001.         //dump($user->getPromotion()); die;
  1002.         // {# #} Vérifier qu’il n’y a pas déjà une promo en cours
  1003.         if ($user->getPromotion()) {
  1004.             $this->addFlash('danger''Ce client possède déjà une promotion active. Veuillez la stopper avant d’en appliquer une nouvelle.');
  1005.             return $this->redirectToRoute('user_show', [
  1006.                 'id'  => $user->getId(),
  1007.                 'tab' => $tab,
  1008.             ]);
  1009.         }
  1010.         // {# #} Appliquer la promotion
  1011.         $user->setPromotion($promotion);
  1012.         // Historique
  1013.         $history = new UserPromotionHistory();
  1014.         $history
  1015.             ->setUser($user)
  1016.             ->setPromotion($promotion)
  1017.             ->setStartedAt($now)
  1018.             ->setStartedBy($operator);
  1019.         $em->persist($history);
  1020.         // Incrémenter le compteur d’utilisation du code
  1021.         if ($promotion->getQuantityUser() !== null) {
  1022.             $promotion->setQuantityUser($promotion->getQuantityUser() + 1);
  1023.         }
  1024.         $em->flush();
  1025.         $this->addFlash('success''Code promotionnel appliqué avec succès.');
  1026.         return $this->redirectToRoute('user_show', [
  1027.             'id'  => $user->getId(),
  1028.             'tab' => 'promotion',
  1029.         ]);
  1030.     }
  1031.     /**
  1032.      * @Route("/admin/user/{id}/end-promotion", name="user_end_promotion", methods={"POST"})
  1033.      */
  1034.     public function endPromotion(User $userRequest $requestEntityManagerInterface $emUserPromotionHistoryRepository $historyRepo) {
  1035.         $promo $user->getPromotion();
  1036.         if ($promo) {
  1037.             // récupérer la dernière ligne d'historique non terminée
  1038.             $last $historyRepo->findOneBy(
  1039.                 ['user' => $user'promotion' => $promo'endedAt' => null],
  1040.                 ['startedAt' => 'DESC']
  1041.             );
  1042.             if ($last) {
  1043.                 $now = new \DateTimeImmutable();
  1044.                 $operator $this->getUser();
  1045.                 $last->setEndedAt($now);
  1046.                 $last->setEndedBy($operator $operator->getFirstName() : 'Système');
  1047.                 // motif optionnel venant de la modale
  1048.                 $reason $request->request->get('form_end_reason');
  1049.                 $note   $request->request->get('form_end_note');
  1050.                 $last->setEndReason($reason ?: null);
  1051.                 $last->setEndNote($note ?: null);
  1052.             }
  1053.             // désactiver le code promo du client
  1054.             $user->setPromotion(null);
  1055.             $em->flush();
  1056.         }
  1057.         $this->addFlash('success''Le code promotionnel a été retiré pour ce client.');
  1058.         return $this->redirectToRoute('user_show', [
  1059.             'id'  => $user->getId(),
  1060.             'tab' => 'promotion'
  1061.         ]);
  1062.     }
  1063.     /**
  1064.      * @Route("/admin/user/{id}/stats/first-last-year", name="client_stats_first_last_year", methods={"GET"})
  1065.      */
  1066.     public function clientFirstLastYear(User $userConnection $conn): JsonResponse
  1067.     {
  1068.         $row $conn->fetchAssociative("
  1069.             SELECT MIN(YEAR(d.created_at)) AS start_year, MAX(YEAR(d.created_at)) AS end_year
  1070.             FROM document d
  1071.             WHERE d.category='client' AND d.type='commande' AND d.client_id=:uid
  1072.         ", ['uid' => $user->getId()]);
  1073.         $current = (int)date('Y');
  1074.         $start = ($row && $row['start_year']) ? (int)$row['start_year'] : $current;
  1075.         $end   = ($row && $row['end_year'])   ? (int)$row['end_year']   : $current;
  1076.         if ($start $end) { $start $end; }
  1077.         return $this->json(['start_year' => $start'end_year' => $end]);
  1078.     }
  1079.         
  1080.    /**
  1081.      * @Route("/admin/user/{id}/stats/sales-trend", name="client_stats_sales_trend", methods={"GET"})
  1082.      */
  1083.     public function clientSalesTrend(User $userRequest $requestEntityManagerInterface $em): JsonResponse
  1084.     {
  1085.         $start       $request->query->get('startDate');
  1086.         $end         $request->query->get('endDate');
  1087.         $basis       strtolower($request->query->get('priceBasis''ht'));         // ht | ttc
  1088.         $granularity strtolower($request->query->get('granularity''month'));     // day | month | year
  1089.         $conn $em->getConnection();
  1090.         $groupFormat = match ($granularity) {
  1091.             'day'   => '%Y-%m-%d',
  1092.             'week'  => '%Y-%u',
  1093.             'year'  => '%Y',
  1094.             default => '%Y-%m'
  1095.         };
  1096.         $caExpr = ($basis === 'ttc')
  1097.             ? "COALESCE(d.total_amount_ttc,0) - COALESCE(d.delivery_company_total, 0)"
  1098.             "COALESCE(d.total_amount_ttc,0) - COALESCE(d.total_tva,0) - COALESCE(d.delivery_company_total, 0)";
  1099.         $netSales "($caExpr - COALESCE(d.delivery_company_total,0))";
  1100.         $cogsSub = ($basis === 'ttc')
  1101.             ? "
  1102.                 SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0) * (1 + COALESCE(t.number,0)/100), 0))
  1103.                 FROM document_declination_produit pdv
  1104.                 JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1105.                 JOIN produit p ON p.id = dv.produit_id
  1106.                 LEFT JOIN tva t ON t.id = pdv.tva_id
  1107.                 WHERE pdv.document_id = d.id
  1108.             "
  1109.             "
  1110.                 SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0), 0))
  1111.                 FROM document_declination_produit pdv
  1112.                 JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1113.                 JOIN produit p ON p.id = dv.produit_id
  1114.                 WHERE pdv.document_id = d.id
  1115.             ";
  1116.         $sql "
  1117.             SELECT
  1118.                 DATE_FORMAT(d.created_at, :groupFormat) AS periode,
  1119.                 ROUND(SUM($netSales), 2) AS ca_ht,
  1120.                 SUM(COALESCE(d.total_tva, 0)) AS tva,
  1121.                 ROUND(SUM($netSales - COALESCE(($cogsSub), 0)), 2) AS marge,
  1122.                 COUNT(DISTINCT d.id) AS orders
  1123.             FROM document d
  1124.             WHERE d.client_id = :uid
  1125.             AND d.type = 'commande'
  1126.             AND d.category = 'client'
  1127.             AND d.status IN ('expedie', 'livre', 'paye', 'facture')
  1128.         ";
  1129.         $params = [
  1130.             'uid'         => $user->getId(),
  1131.             'groupFormat' => $groupFormat,
  1132.         ];
  1133.         // ✅ Ajout conditionnel du filtre de période
  1134.         if (!empty($start)) {
  1135.             $sql .= " AND d.created_at >= :start";
  1136.             $params['start'] = $start ' 00:00:00';
  1137.         }
  1138.         if (!empty($end)) {
  1139.             $sql .= " AND d.created_at <= :end";
  1140.             $params['end'] = $end ' 23:59:59';
  1141.         }
  1142.         $sql .= " GROUP BY periode ORDER BY periode ASC";
  1143.         $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1144.         // Traitement post-requête
  1145.         foreach ($rows as &$r) {
  1146.             $r['ca_ht']         = (float) ($r['ca_ht'] ?? 0);
  1147.             $r['tva']           = (float) ($r['tva'] ?? 0);
  1148.             $r['marge']         = (float) ($r['marge'] ?? 0);
  1149.             $r['orders']        = (int)   ($r['orders'] ?? 0);
  1150.             $r['ca_ttc']        = $r['ca_ht'] + $r['tva'];
  1151.             $r['marge_percent'] = $r['ca_ht'] > round($r['marge'] / $r['ca_ht'] * 1002) : 0.0;
  1152.         }
  1153.         unset($r);
  1154.         return new JsonResponse($rows);
  1155.     }
  1156.     /**
  1157.      * @Route("/admin/user/{id}/stats/by-source", name="client_stats_by_source", methods={"GET"})
  1158.      */
  1159.     public function clientSalesBySource(User $userRequest $requestConnection $connEntityManagerInterface $em): JsonResponse
  1160.     {
  1161.         $start  $request->query->get('startDate');
  1162.         $end    $request->query->get('endDate');
  1163.         $basis  strtolower($request->query->get('priceBasis''ht'));
  1164.         $metric strtolower($request->query->get('metric''orders')); // orders | ca | margin | ca_margin
  1165.         /* --- CA net --- */
  1166.         $netSales = ($basis === 'ttc')
  1167.             ? "COALESCE(d.total_amount_ttc,0) - COALESCE(d.delivery_company_total,0)"
  1168.             "COALESCE(d.total_amount_ttc,0) - COALESCE(d.total_tva,0) - COALESCE(d.delivery_company_total,0)";
  1169.         /* --- COGS document --- */
  1170.         $cogsSub = ($basis === 'ttc')
  1171.             ? "
  1172.                 SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0) * (1 + COALESCE(t.number,0)/100),0))
  1173.                 FROM document_declination_produit pdv
  1174.                 JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1175.                 JOIN produit p ON p.id = dv.produit_id
  1176.                 LEFT JOIN tva t ON t.id = pdv.tva_id
  1177.                 WHERE pdv.document_id = d.id
  1178.             "
  1179.             "
  1180.                 SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0),0))
  1181.                 FROM document_declination_produit pdv
  1182.                 JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1183.                 JOIN produit p ON p.id = dv.produit_id
  1184.                 WHERE pdv.document_id = d.id
  1185.             ";
  1186.         /* --- SQL --- */
  1187.         $sql "
  1188.             SELECT
  1189.                 s.name AS name,
  1190.                 COALESCE(COUNT(DISTINCT d.id), 0) AS orders,
  1191.                 COALESCE(SUM($netSales), 0) AS ca,
  1192.                 COALESCE(SUM($netSales - COALESCE(($cogsSub), 0)), 0) AS margin
  1193.             FROM source s
  1194.             LEFT JOIN document d
  1195.                 ON d.source_id = s.id
  1196.                 AND d.client_id = :uid
  1197.                 AND d.type = 'commande'
  1198.                 AND d.status IN ('expedie', 'livre', 'paye', 'facture')
  1199.         ";
  1200.         $params = ['uid' => $user->getId()];
  1201.         // ✅ Ajout conditionnel du filtre de période
  1202.         if (!empty($start)) {
  1203.             $sql .= " AND d.created_at >= :start";
  1204.             $params['start'] = $start ' 00:00:00';
  1205.         }
  1206.         if (!empty($end)) {
  1207.             $sql .= " AND d.created_at <= :end";
  1208.             $params['end'] = $end ' 23:59:59';
  1209.         }
  1210.         $sql .= "
  1211.             GROUP BY s.id, s.name
  1212.             ORDER BY orders DESC, s.name ASC
  1213.         ";
  1214.         $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1215.         $labels array_column($rows'name');
  1216.         // ✅ Récupération des couleurs personnalisées
  1217.         $colorMap $em->getRepository(\App\Entity\Source::class)->findColorMap();
  1218.         $colors = [];
  1219.         foreach ($labels as $lab) {
  1220.             $key trim(mb_strtolower((string)$lab'UTF-8'));
  1221.             $colors[] = $colorMap[$key] ?? null;
  1222.         }
  1223.         // ✅ CA + Marge
  1224.         if ($metric === 'ca_margin') {
  1225.             return new JsonResponse([
  1226.                 'labels'        => $labels,
  1227.                 'ca_values'     => array_map('floatval'array_column($rows'ca')),
  1228.                 'margin_values' => array_map('floatval'array_column($rows'margin')),
  1229.                 'colors'        => $colors,
  1230.             ]);
  1231.         }
  1232.         // ✅ Cas standard
  1233.         $key = match ($metric) {
  1234.             'orders' => 'orders',
  1235.             'margin' => 'margin',
  1236.             default  => 'ca',
  1237.         };
  1238.         return new JsonResponse([
  1239.             'labels' => $labels,
  1240.             'values' => array_map(fn($r) => (float)($r[$key] ?? 0), $rows),
  1241.             'colors' => $colors,
  1242.         ]);
  1243.     }
  1244.    /**
  1245.      * @Route("/admin/user/{id}/stats/by-category", name="client_stats_by_category", methods={"GET"})
  1246.      */
  1247.     public function getClientSalesByCategory(Request $requestint $idConnection $conn): JsonResponse
  1248.     {
  1249.         $start  $request->query->get('startDate');
  1250.         $end    $request->query->get('endDate');
  1251.         $basis  strtolower($request->query->get('priceBasis''ht'));       // ht | ttc
  1252.         $metric strtolower($request->query->get('metric''qty'));          // qty | orders | ca | margin | ca_margin
  1253.         $netSales = ($basis === 'ttc')
  1254.             ? "COALESCE(d.total_amount_ttc,0) - COALESCE(d.delivery_company_total,0)"
  1255.             "COALESCE(d.total_amount_ttc,0) - COALESCE(d.total_tva,0) - COALESCE(d.delivery_company_total,0)";
  1256.         $sql "
  1257.             SELECT
  1258.                 c.name AS name,
  1259.                 COUNT(DISTINCT d.id) AS orders,
  1260.                 SUM(pdv.quantity) AS qty,
  1261.                 SUM($netSales) AS ca,
  1262.                 SUM(
  1263.                     pdv.quantity *
  1264.                     COALESCE(
  1265.                         COALESCE(p.buying_price_ht, 0) * (1 + COALESCE(t.number, 0)/100),
  1266.                         0
  1267.                     )
  1268.                 ) AS cost
  1269.             FROM document d
  1270.             JOIN document_declination_produit pdv ON pdv.document_id = d.id
  1271.             JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1272.             JOIN produit p ON p.id = dv.produit_id
  1273.             LEFT JOIN category c ON c.id = p.categories_id
  1274.             LEFT JOIN tva t ON t.id = pdv.tva_id
  1275.             WHERE d.client_id = :clientId
  1276.             AND d.type = 'commande'
  1277.             AND d.status IN ('expedie', 'livre', 'paye', 'facture')
  1278.         ";
  1279.         $params = ['clientId' => $id];
  1280.         // ✅ Application conditionnelle du filtre temporel
  1281.         if (!empty($start)) {
  1282.             $sql .= " AND d.created_at >= :start";
  1283.             $params['start'] = $start ' 00:00:00';
  1284.         }
  1285.         if (!empty($end)) {
  1286.             $sql .= " AND d.created_at <= :end";
  1287.             $params['end'] = $end ' 23:59:59';
  1288.         }
  1289.         $sql .= "
  1290.             GROUP BY c.id, c.name
  1291.             ORDER BY qty DESC, c.name ASC
  1292.         ";
  1293.         $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1294.         $labels array_column($rows'name');
  1295.         // ✅ Cas spécial : CA + Marge
  1296.         if ($metric === 'ca_margin') {
  1297.             return new JsonResponse([
  1298.                 'labels'        => $labels,
  1299.                 'ca_values'     => array_map(fn($r) => (float)($r['ca'] ?? 0), $rows),
  1300.                 'margin_values' => array_map(fn($r) =>
  1301.                     (float)($r['ca'] ?? 0) - (float)($r['cost'] ?? 0), $rows),
  1302.             ]);
  1303.         }
  1304.         // ✅ Cas standards : qty | orders | ca | margin
  1305.         $values = match ($metric) {
  1306.             'qty'    => array_map(fn($r) => (float)($r['qty'] ?? 0), $rows),
  1307.             'orders' => array_map(fn($r) => (float)($r['orders'] ?? 0), $rows),
  1308.             'margin' => array_map(fn($r) =>
  1309.                 (float)($r['ca'] ?? 0) - (float)($r['cost'] ?? 0), $rows),
  1310.             default  => array_map(fn($r) => (float)($r['ca'] ?? 0), $rows),
  1311.         };
  1312.         return new JsonResponse([
  1313.             'labels' => $labels,
  1314.             'values' => $values,
  1315.         ]);
  1316.     }
  1317.    /**
  1318.      * @Route("/admin/client/{id}/stats/by-status", name="client_stats_by_status", methods={"GET"})
  1319.      */
  1320.     public function clientStatsByStatus(User $userRequest $requestConnection $conn): JsonResponse
  1321.     {
  1322.         $start  $request->query->get('startDate');
  1323.         $end    $request->query->get('endDate');
  1324.         $metric strtolower($request->query->get('metric''orders')); // orders | ca | margin | ca+margin
  1325.         $basis  strtolower($request->query->get('priceBasis''ht'));
  1326.         $netSales = ($basis === 'ttc')
  1327.             ? "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.delivery_company_total, 0)"
  1328.             "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0)";
  1329.         $cogsSub = ($basis === 'ttc')
  1330.             ? "
  1331.                 SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0) * (1 + COALESCE(t.number,0)/100), 0))
  1332.                 FROM document_declination_produit pdv
  1333.                 JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1334.                 JOIN produit p ON p.id = dv.produit_id
  1335.                 LEFT JOIN tva t ON t.id = pdv.tva_id
  1336.                 WHERE pdv.document_id = d.id
  1337.             "
  1338.             "
  1339.                 SELECT SUM(pdv.quantity * COALESCE(COALESCE(dv.buying_price_ht, p.buying_price_ht, 0), 0))
  1340.                 FROM document_declination_produit pdv
  1341.                 JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1342.                 JOIN produit p ON p.id = dv.produit_id
  1343.                 WHERE pdv.document_id = d.id
  1344.             ";
  1345.         $sql "
  1346.             SELECT
  1347.                 d.status AS statut,
  1348.                 COUNT(*) AS orders,
  1349.                 SUM($netSales) AS ca,
  1350.                 SUM($netSales - COALESCE(($cogsSub), 0)) AS margin
  1351.             FROM document d
  1352.             WHERE d.client_id = :clientId
  1353.             AND d.type = 'commande'
  1354.         ";
  1355.         $params = ['clientId' => $user->getId()];
  1356.         // ✅ Ajout des filtres dynamiques de période
  1357.         if (!empty($start)) {
  1358.             $sql .= " AND d.created_at >= :start";
  1359.             $params['start'] = $start ' 00:00:00';
  1360.         }
  1361.         if (!empty($end)) {
  1362.             $sql .= " AND d.created_at <= :end";
  1363.             $params['end'] = $end ' 23:59:59';
  1364.         }
  1365.         $sql .= " GROUP BY d.status ORDER BY orders DESC";
  1366.         $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1367.         $labels array_column($rows'statut');
  1368.         if ($metric === 'ca+margin') {
  1369.             return new JsonResponse([
  1370.                 'labels'        => $labels,
  1371.                 'ca_values'     => array_map(fn($r) => (float)($r['ca'] ?? 0), $rows),
  1372.                 'margin_values' => array_map(fn($r) =>
  1373.                     (float)($r['ca'] ?? 0) - (float)($r['margin'] ?? 0), $rows),
  1374.             ]);
  1375.         }
  1376.         $key = match ($metric) {
  1377.             'ca'     => 'ca',
  1378.             'margin' => 'margin',
  1379.             default  => 'orders'
  1380.         };
  1381.         return new JsonResponse([
  1382.             'labels' => $labels,
  1383.             'values' => array_map(fn($r) => (float)($r[$key] ?? 0), $rows),
  1384.         ]);
  1385.     }
  1386.     /**
  1387.      * @Route("/admin/client/{id}/stats/repurchase-delay", name="client_stats_repurchase_delay", methods={"GET"})
  1388.      */
  1389.     public function getRepurchaseDelay(int $idRequest $requestEntityManagerInterface $em): JsonResponse
  1390.     {
  1391.         $start       $request->query->get('startDate');
  1392.         $end         $request->query->get('endDate');
  1393.         $granularity $request->query->get('granularity''month');
  1394.         $format      $granularity === 'day' '%Y-%m-%d' '%Y-%m';
  1395.         $params = [
  1396.             'clientId' => $id,
  1397.             'format'   => $format,
  1398.         ];
  1399.         // Construction dynamique du filtre date
  1400.         $dateFilter '';
  1401.         if (!empty($start)) {
  1402.             $dateFilter .= " AND d.created_at >= :start";
  1403.             $params['start'] = $start ' 00:00:00';
  1404.         }
  1405.         if (!empty($end)) {
  1406.             $dateFilter .= " AND d.created_at <= :end";
  1407.             $params['end'] = $end ' 23:59:59';
  1408.         }
  1409.         // Sous-requête avec lag() : calcule le délai entre deux commandes successives
  1410.         $innerSql "
  1411.             SELECT
  1412.                 DATE_FORMAT(d.created_at, :format) AS periode,
  1413.                 DATEDIFF(d.created_at, LAG(d.created_at) OVER (ORDER BY d.created_at)) AS delay
  1414.             FROM document d
  1415.             WHERE d.client_id = :clientId
  1416.             AND d.type = 'commande'
  1417.             AND d.status IN ('expedie','livre','paye','facture')
  1418.             $dateFilter
  1419.         ";
  1420.         // Requête principale : moyenne du délai par période
  1421.         $sql "
  1422.             SELECT
  1423.                 periode,
  1424.                 ROUND(AVG(delay), 1) AS avg_delay
  1425.             FROM (
  1426.                 $innerSql
  1427.             ) sub
  1428.             WHERE delay IS NOT NULL
  1429.             GROUP BY periode
  1430.             ORDER BY periode
  1431.         ";
  1432.         $stmt $em->getConnection()->prepare($sql);
  1433.         $rows $stmt->executeQuery($params)->fetchAllAssociative();
  1434.         return $this->json([
  1435.             'labels' => array_column($rows'periode'),
  1436.             'values' => array_map(fn($r) => (float)($r['avg_delay'] ?? 0), $rows),
  1437.         ]);
  1438.     }
  1439.     /**
  1440.      * @Route("/admin/client/{id}/stats/aov", name="client_stats_aov", methods={"GET"})
  1441.      */
  1442.    public function getClientAverageOrderValueChart(Request $requestUser $userConnection $conn): JsonResponse {
  1443.     $start       $request->query->get('startDate');
  1444.     $end         $request->query->get('endDate');
  1445.     $granularity $request->query->get('granularity''month');
  1446.     $basis       strtolower($request->query->get('priceBasis''ht'));
  1447.     $groupFormat = match ($granularity) {
  1448.         'day'   => '%Y-%m-%d',
  1449.         'week'  => '%Y-%u',
  1450.         'month' => '%Y-%m',
  1451.         'year'  => '%Y',
  1452.         default => '%Y-%m'
  1453.     };
  1454.     $netSalesExpr $basis === 'ttc'
  1455.         "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.delivery_company_total, 0)"
  1456.         "COALESCE(d.total_amount_ttc, 0) - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0)";
  1457.     $params = [
  1458.         'uid'         => $user->getId(),
  1459.         'groupFormat' => $groupFormat,
  1460.     ];
  1461.     $dateFilter '';
  1462.     if (!empty($start)) {
  1463.         $dateFilter .= " AND d.created_at >= :start";
  1464.         $params['start'] = $start ' 00:00:00';
  1465.     }
  1466.     if (!empty($end)) {
  1467.         $dateFilter .= " AND d.created_at <= :end";
  1468.         $params['end'] = $end ' 23:59:59';
  1469.     }
  1470.     $sql "
  1471.         SELECT
  1472.             DATE_FORMAT(d.created_at, :groupFormat) AS period,
  1473.             ROUND(SUM($netSalesExpr) / COUNT(DISTINCT d.id), 2) AS aov
  1474.         FROM document d
  1475.         WHERE d.client_id = :uid
  1476.           AND d.type = 'commande'
  1477.           AND d.status IN ('expedie', 'livre', 'paye', 'facture')
  1478.           $dateFilter
  1479.         GROUP BY period
  1480.         ORDER BY period ASC
  1481.     ";
  1482.     $res $conn->prepare($sql)->executeQuery($params);
  1483.     $rows method_exists($res'fetchAllAssociative') ? $res->fetchAllAssociative() : $res->fetchAll();
  1484.     return new JsonResponse([
  1485.         'labels' => array_column($rows'period'),
  1486.         'values' => array_map(fn($r) => (float)$r['aov'], $rows),
  1487.     ]);
  1488. }
  1489.    /**
  1490.      * @Route("/admin/user/{id}/stats/exchange", name="client_stats_exchange", methods={"GET"})
  1491.      */
  1492.     public function clientExchangeRate(int $idRequest $requestConnection $conn): JsonResponse {
  1493.         $start       $request->query->get('startDate');
  1494.         $end         $request->query->get('endDate');
  1495.         $granularity strtolower($request->query->get('granularity''month'));
  1496.         $groupFormat = match ($granularity) {
  1497.             'day'   => '%Y-%m-%d',
  1498.             'week'  => '%Y-%u',
  1499.             'year'  => '%Y',
  1500.             default => '%Y-%m'
  1501.         };
  1502.         $params = [
  1503.             'clientId'    => $id,
  1504.             'groupFormat' => $groupFormat,
  1505.         ];
  1506.         $dateFilter '';
  1507.         if (!empty($start)) {
  1508.             $dateFilter .= " AND d.created_at >= :start";
  1509.             $params['start'] = $start ' 00:00:00';
  1510.         }
  1511.         if (!empty($end)) {
  1512.             $dateFilter .= " AND d.created_at <= :end";
  1513.             $params['end'] = $end ' 23:59:59';
  1514.         }
  1515.         $sql "
  1516.             SELECT
  1517.                 DATE_FORMAT(d.created_at, :groupFormat) AS periode,
  1518.                 COUNT(DISTINCT CASE 
  1519.                     WHEN d.type = 'commande' AND d.status IN ('expedie','livre','paye','facture') THEN d.id
  1520.                 END) AS valid_orders,
  1521.                 COUNT(DISTINCT CASE 
  1522.                     WHEN d.type = 'echange' AND d.status != 'annule' THEN d.id
  1523.                 END) AS exchanges
  1524.             FROM document d
  1525.             WHERE d.client_id = :clientId
  1526.             AND (
  1527.                 (d.type = 'commande' AND d.status IN ('expedie','livre','paye','facture'))
  1528.                 OR (d.type = 'echange' AND d.status != 'annule')
  1529.             )
  1530.             $dateFilter
  1531.             GROUP BY DATE_FORMAT(d.created_at, :groupFormat)
  1532.             ORDER BY DATE_FORMAT(d.created_at, :groupFormat) ASC
  1533.         ";
  1534.         $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1535.         $labels    = [];
  1536.         $exchanges = [];
  1537.         $totals    = [];
  1538.         $rates     = [];
  1539.         foreach ($rows as $r) {
  1540.             $valid    = (int)($r['valid_orders'] ?? 0);
  1541.             $exchange = (int)($r['exchanges'] ?? 0);
  1542.             $rate     $valid round($exchange $valid 1001) : 0;
  1543.             $labels[]    = $r['periode'];
  1544.             $exchanges[] = $exchange;
  1545.             $totals[]    = $valid;
  1546.             $rates[]     = $rate;
  1547.         }
  1548.         return new JsonResponse([
  1549.             'labels'    => $labels,
  1550.             'exchanges' => $exchanges,
  1551.             'totals'    => $totals,
  1552.             'rates'     => $rates
  1553.         ]);
  1554.     }
  1555. /**
  1556.  * @Route("/admin/user/{id}/stats/by-declination", name="client_stats_by_declination", methods={"GET"})
  1557.  */
  1558. public function clientStatsByDeclination(int $idRequest $requestConnection $conn): JsonResponse {
  1559.     $start $request->query->get('startDate');
  1560.     $end   $request->query->get('endDate');
  1561.     $params = ['clientId' => $id];
  1562.     $dateFilter '';
  1563.     if (!empty($start)) {
  1564.         $dateFilter .= " AND d.created_at >= :start";
  1565.         $params['start'] = $start ' 00:00:00';
  1566.     }
  1567.     if (!empty($end)) {
  1568.         $dateFilter .= " AND d.created_at <= :end";
  1569.         $params['end'] = $end ' 23:59:59';
  1570.     }
  1571.     $sql "
  1572.         SELECT 
  1573.             dcl.name AS declinaison,
  1574.             v.name AS valeur,
  1575.             SUM(ddp.quantity) AS total_qty
  1576.         FROM document d
  1577.         JOIN document_declination_produit ddp ON ddp.document_id = d.id
  1578.         JOIN produit_declination_value pdv ON ddp.produit_declination_value_id = pdv.id
  1579.         JOIN group_declination_value gdv ON gdv.produit_declination_id = pdv.id
  1580.         JOIN value_declination v ON v.id = gdv.value_id
  1581.         JOIN declination dcl ON dcl.id = v.declination_id
  1582.         WHERE d.client_id = :clientId
  1583.         AND d.type = 'commande'
  1584.         AND d.status IN ('expedie', 'livre', 'paye', 'facture')
  1585.         $dateFilter
  1586.         GROUP BY dcl.name, v.name
  1587.         ORDER BY dcl.name ASC, total_qty DESC
  1588.     ";
  1589.     $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1590.     $dataGrouped = [];  // [declinaison => [[label, value], ...]]
  1591.     foreach ($rows as $r) {
  1592.         $declinaison $r['declinaison'];
  1593.         $valeur $r['valeur'];
  1594.         $qty = (int)$r['total_qty'];
  1595.         if (!isset($dataGrouped[$declinaison])) {
  1596.             $dataGrouped[$declinaison] = [];
  1597.         }
  1598.         $dataGrouped[$declinaison][] = [
  1599.             'label' => $valeur,
  1600.             'value' => $qty
  1601.         ];
  1602.     }
  1603.     return new JsonResponse([
  1604.         'data' => $dataGrouped// => { "Couleur": [...], "Taille": [...] }
  1605.     ]);
  1606. }
  1607. /**
  1608.  * @Route("/admin/user/{id}/stats/repeated-products", name="client_stats_repeated_products", methods={"GET"})
  1609.  */
  1610. public function getClientRepeatedProducts(int $idRequest $requestConnection $conn): JsonResponse {
  1611.     $start $request->query->get('startDate');
  1612.     $end   $request->query->get('endDate');
  1613.     $limit = (int)$request->query->get('limit'10);
  1614.     if ($limit 1$limit 10;
  1615.     $params = ['clientId' => $id];
  1616.     $dateFilter '';
  1617.     if (!empty($start)) {
  1618.         $dateFilter .= " AND d.created_at >= :start";
  1619.         $params['start'] = $start ' 00:00:00';
  1620.     }
  1621.     if (!empty($end)) {
  1622.         $dateFilter .= " AND d.created_at <= :end";
  1623.         $params['end'] = $end ' 23:59:59';
  1624.     }
  1625.     $sql "
  1626.         SELECT
  1627.             p.name AS label,
  1628.             COUNT(d.id) AS total_orders,
  1629.             COUNT(DISTINCT d.id) AS unique_orders
  1630.         FROM document d
  1631.         JOIN document_declination_produit pdv ON pdv.document_id = d.id
  1632.         JOIN produit_declination_value dv ON dv.id = pdv.produit_declination_value_id
  1633.         JOIN produit p ON p.id = dv.produit_id
  1634.         WHERE d.client_id = :clientId
  1635.         AND d.type = 'commande'
  1636.         AND d.status IN ('expedie','livre','paye','facture')
  1637.         $dateFilter
  1638.         GROUP BY p.id, p.name
  1639.         HAVING COUNT(DISTINCT d.id) >= 2
  1640.         ORDER BY total_orders DESC
  1641.         LIMIT $limit
  1642.     ";
  1643.     $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1644.     return new JsonResponse([
  1645.         'labels' => array_column($rows'label'),
  1646.         'values' => array_map(fn($r) => (int)($r['total_orders'] ?? 0), $rows),
  1647.     ]);
  1648. }
  1649. /**
  1650.  * @Route("/admin/user/{id}/stats/payment-modes", name="client_stats_payment_modes", methods={"GET"})
  1651.  */
  1652. public function clientStatsPaymentModes(int $idRequest $requestConnection $conn): JsonResponse
  1653. {
  1654.     $start $request->query->get('startDate');
  1655.     $end   $request->query->get('endDate');
  1656.     $basis strtolower($request->query->get('priceBasis''ht')); // ht | ttc
  1657.     $priceField $basis === 'ttc'
  1658.         '(d.total_amount_ttc - COALESCE(d.delivery_company_total, 0))'
  1659.         '(d.total_amount_ttc - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0))';
  1660.     $sql "
  1661.         SELECT
  1662.             COALESCE(d.payment_method, 'Inconnu') AS mode,
  1663.             COUNT(*) AS orders,
  1664.             SUM($priceField) AS total
  1665.         FROM document d
  1666.         WHERE d.client_id = :clientId
  1667.           AND d.type = 'commande'
  1668.           AND d.status IN ('expedie','livre','paye','facture')
  1669.     ";
  1670.     $params = ['clientId' => $id];
  1671.     if (!empty($start)) {
  1672.         $sql .= " AND d.created_at >= :start";
  1673.         $params['start'] = $start ' 00:00:00';
  1674.     }
  1675.     if (!empty($end)) {
  1676.         $sql .= " AND d.created_at <= :end";
  1677.         $params['end'] = $end ' 23:59:59';
  1678.     }
  1679.     $sql .= " GROUP BY mode ORDER BY orders DESC";
  1680.     $rows $conn->prepare($sql)->executeQuery($params)->fetchAllAssociative();
  1681.     return new JsonResponse([
  1682.         'labels' => array_column($rows'mode'),
  1683.         'orders' => array_map(fn($r) => (int) $r['orders'], $rows),
  1684.         'totals' => array_map(fn($r) => round((float) $r['total'], 2), $rows),
  1685.     ]);
  1686. }
  1687. /**
  1688.  * @Route("/admin/user/{id}/stats/promo-vs-full", name="client_stats_promo_vs_full", methods={"GET"})
  1689.  */
  1690. public function getPromoVsFullPriceStats(int $idRequest $requestConnection $conn): JsonResponse
  1691. {
  1692.     $start $request->query->get('startDate');
  1693.     $end   $request->query->get('endDate');
  1694.     $basis strtolower($request->query->get('priceBasis''ht')); // ht | ttc
  1695.     // ✅ Base de calcul propre (hors remise et livraison)
  1696.     $priceField $basis === 'ttc'
  1697.         '(d.total_amount_ttc - COALESCE(d.delivery_company_total, 0))'
  1698.         '(d.total_amount_ttc - COALESCE(d.total_tva, 0) - COALESCE(d.delivery_company_total, 0))';
  1699.     $params = ['clientId' => $id];
  1700.     $dateFilter '';
  1701.     if (!empty($start)) {
  1702.         $dateFilter .= " AND d.created_at >= :start";
  1703.         $params['start'] = $start ' 00:00:00';
  1704.     }
  1705.     if (!empty($end)) {
  1706.         $dateFilter .= " AND d.created_at <= :end";
  1707.         $params['end'] = $end ' 23:59:59';
  1708.     }
  1709.     $sql "
  1710.         SELECT
  1711.             SUM(CASE WHEN d.discount > 0 THEN $priceField ELSE 0 END) AS promo_total,
  1712.             SUM(CASE WHEN d.discount = 0 OR d.discount IS NULL THEN $priceField ELSE 0 END) AS full_total
  1713.         FROM document d
  1714.         WHERE d.client_id = :clientId
  1715.           AND d.type = 'commande'
  1716.           AND d.status IN ('expedie','livre','paye','facture')
  1717.           $dateFilter
  1718.     ";
  1719.     $row $conn->prepare($sql)->executeQuery($params)->fetchAssociative();
  1720.     $promoTotal = (float)($row['promo_total'] ?? 0);
  1721.     $fullTotal  = (float)($row['full_total'] ?? 0);
  1722.     $total      $promoTotal $fullTotal;
  1723.     return new JsonResponse([
  1724.         'labels'  => ['Promo''Plein tarif'],
  1725.         'values'  => [$promoTotal$fullTotal],
  1726.         'percent' => [
  1727.             $total round($promoTotal $total 1001) : 0,
  1728.             $total round($fullTotal  $total 1001) : 0
  1729.         ]
  1730.     ]);
  1731. }
  1732.     
  1733. }