src/Controller/Api/ConversationsController.php line 56

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api;
  3. use App\Entity\Core\Notifications;
  4. use App\Entity\Core\Users;
  5. use App\Entity\Cvs\Candidates;
  6. use App\Entity\Messenger\Groups;
  7. use App\Entity\Messenger\Messages;
  8. use App\Services\Dossiers;
  9. use App\Services\NotificationService;
  10. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
  11. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  12. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\Routing\Annotation\Route;
  18. use Symfony\Component\HttpFoundation\JsonResponse;
  19. use Doctrine\ORM\EntityManagerInterface;
  20. use Knp\Component\Pager\PaginatorInterface;
  21. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  22. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  23. use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
  24. use Symfony\Component\Form\Extension\Core\Type\EmailType;
  25. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  26. use Symfony\Component\Form\Extension\Core\Type\NumberType;
  27. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  28. use Symfony\Component\Form\Extension\Core\Type\TextareaType;
  29. use Symfony\Component\Form\Extension\Core\Type\TextType;
  30. use Symfony\Component\HttpFoundation\Session\Session;
  31. class ConversationsController extends AbstractController
  32. {
  33.     private $em;
  34.     private $paginator;
  35.     private $dossier;
  36.     private $notificationService;
  37.     public function __construct(EntityManagerInterface $em,
  38.                                 Dossiers $dossier,
  39.                                 PaginatorInterface $paginator,
  40.                                 NotificationService $notificationService
  41.     ) {
  42.         $this->em $em;
  43.         $this->paginator $paginator;
  44.         $this->dossier $dossier;
  45.         $this->notificationService $notificationService;
  46.     }
  47.     /**
  48.      * Liste des conversations récentes de l'utilisateur connecté
  49.      */
  50.     public function recents(Request $request): JsonResponse
  51.     {
  52.         $user $this->getUser();
  53.         $limit $request->query->get('limit'20);
  54.         $conversations $this->em->getRepository(Groups::class)->recents($user$limit);
  55.         $array = [];
  56.         foreach ($conversations as $conversation) {
  57.             $otherUser $conversation->getOtherUser($user);
  58.             if (!$otherUser) {
  59.                 continue;
  60.             }
  61.             $countMessages $this->em->getRepository(Messages::class)->countByConversation($conversation);
  62.             $fullname =  $otherUser->getName()." "$otherUser->getLastname();
  63.             $avatar "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjEwMCIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCwgMTAwKSI+CiAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iLTE1IiByPSIzNSIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgICA8cGF0aCBkPSJNIC01MCA2NSBRIC01MCAyNSAtMzUgMTAgUSAtMTUgLTUgMCAtNSBRIDE1IC01IDM1IDEwIFEgNTAgMjUgNTAgNjUgWiIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgPC9nPgo8L3N2Zz4K";
  64.             if ($otherUser->getImage()) {
  65.                 $avatar $otherUser->getImageBase64();
  66.             }
  67.             $array[] = [
  68.                 "id" => (string)$conversation->getId(),
  69.                 "conversationHash" => $conversation->getConversationHash(),
  70.                 "title" => $conversation->getTitle() ?: 'Conversation avec ' $fullname,
  71.                 "fullname" => $fullname,
  72.                 "userId" => $otherUser->getId(),
  73.                 "userEmail" => $otherUser->getEmail(),
  74.                 "avatar" => $avatar,
  75.                 "count_messages" => $countMessages,
  76.                 "open" => false,
  77.                 "lastmessage" => $conversation->getLastMessageContent() ?? '',
  78.                 "lastMessageAt" => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  79.                 "createdAt" => $conversation->getCreatedAt() ? $conversation->getCreatedAt()->format('c') : null,
  80.             ];
  81.         }
  82.         return new JsonResponse($array);
  83.     }
  84.     /**
  85.      * Obtenir la configuration pour une conversation
  86.      */
  87.     public function getConfig(int $id): JsonResponse
  88.     {
  89.         $user $this->getUser();
  90.         if (!$user) {
  91.             return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  92.         }
  93.         $conversation $this->em->getRepository(Groups::class)->find($id);
  94.         if (!$conversation) {
  95.             return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  96.         }
  97.         if (!$conversation->hasUser($user)) {
  98.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  99.         }
  100.         $otherUser $conversation->getOtherUser($user);
  101.         if (!$otherUser) {
  102.             return new JsonResponse(['error' => 'Interlocuteur introuvable'], Response::HTTP_NOT_FOUND);
  103.         }
  104.         $fullname $otherUser->getName()." ".$otherUser->getLastname();
  105.         $avatar "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjEwMCIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCwgMTAwKSI+CiAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iLTE1IiByPSIzNSIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgICA8cGF0aCBkPSJNIC01MCA2NSBRIC01MCAyNSAtMzUgMTAgUSAtMTUgLTUgMCAtNSBRIDE1IC01IDM1IDEwIFEgNTAgMjUgNTAgNjUgWiIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgPC9nPgo8L3N2Zz4K";
  106.         if ($otherUser->getImage()) {
  107.             $avatar $otherUser->getImageBase64();
  108.         }
  109.         $config = [
  110.             'groupId' => (string) $conversation->getId(),
  111.             'userId' => $user->getId(),
  112.             'conversationHash' => $conversation->getConversationHash(),
  113.             'title' => $conversation->getTitle() ?: 'Conversation avec ' $fullname,
  114.             'otherUser' => [
  115.                 'id' => $otherUser->getId(),
  116.                 'name' => $otherUser->getName(),
  117.                 'lastname' => $otherUser->getLastname(),
  118.                 'fullname' => $fullname,
  119.                 'email' => $otherUser->getEmail(),
  120.                 'avatar' => $avatar,
  121.             ],
  122.             'urls' => [
  123.                 'api' => $_ENV["WEBSOCKET_MESSENGER_WS"],
  124.                 'ws' => $_ENV["WEBSOCKET_MESSENGER_WS"],
  125.             ],
  126.             'createdAt' => $conversation->getCreatedAt() ? $conversation->getCreatedAt()->format('c') : null,
  127.             'lastMessageAt' => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  128.         ];
  129.         error_log('✅ [Config] Configuration envoyée pour conversation ' $conversation->getId());
  130.         error_log('   User ID: ' $user->getId());
  131.         error_log('   WS URL: ' $config['urls']['ws']);
  132.         return new JsonResponse($config);
  133.     }
  134.     /**
  135.      * Afficher les messages d'une conversation
  136.      */
  137.     public function show(Request $requestint $id): JsonResponse
  138.     {
  139.         $user $this->getUser();
  140.         $conversation $this->em->getRepository(Groups::class)->find($id);
  141.         if (!$conversation) {
  142.             return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  143.         }
  144.         if (!$conversation->hasUser($user)) {
  145.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  146.         }
  147.         $limit $request->query->get('limit'100);
  148.         $offset $request->query->get('offset'0);
  149.         $messages $this->em->getRepository(Messages::class)->findByConversation($conversation$limit$offset);
  150.         $array = [];
  151.         foreach ($messages as $message) {
  152.             $isOwn $message->getUser()->getId() === $user->getId();
  153.             $fullname =  $message->getUser()->getName()." "$message->getUser()->getLastname();
  154.             $avatar $this->getDefaultAvatar();
  155.             if ($message->getUser()->getImage()) {
  156.                 $avatar $message->getUser()->getImageBase64();
  157.             }
  158.             $array[] = [
  159.                 "id" => (string)$message->getId(),
  160.                 "message" => (string)$message->getMessage(),
  161.                 "isOwn" => $isOwn,
  162.                 "userId" => $message->getUser()->getId(),
  163.                 "userName" => $fullname,
  164.                 "avatar" => $avatar,
  165.                 "createdAt" => $message->getCreatedAt() ? $message->getCreatedAt()->format('c') : null,
  166.             ];
  167.         }
  168.         return new JsonResponse($array);
  169.     }
  170.     /**
  171.      * Envoyer un message
  172.      */
  173.     public function sendMessage(Request $requestint $id): JsonResponse
  174.     {
  175.         $user $this->getUser();
  176.         $conversation $this->em->getRepository(Groups::class)->find($id);
  177.         if (!$conversation) {
  178.             return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  179.         }
  180.         if (!$conversation->hasUser($user)) {
  181.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  182.         }
  183.         $data json_decode($request->getContent(), true);
  184.         $messageText $data['message'] ?? null;
  185.         if (empty($messageText)) {
  186.             return new JsonResponse(['error' => 'Message vide'], Response::HTTP_BAD_REQUEST);
  187.         }
  188.         $emetteur $user;
  189.         $destinataire null;
  190.         if($conversation->getUserOne() !== $user) {
  191.             $destinataire $conversation->getUserOne();
  192.         }
  193.         if($conversation->getUserTwo() !== $user) {
  194.             $destinataire $conversation->getUserTwo();
  195.         }
  196.         // Créer le message
  197.         $message = new Messages();
  198.         $message->setMessage($messageText);
  199.         $message->setUser($user);
  200.         $message->setGroup($conversation);
  201.         $message->setCreatedAt(new \DateTime());
  202.         $this->em->persist($message);
  203.         $this->em->flush();
  204.         // Mettre à jour la conversation (avec le nouveau champ lastMessageSenderId)
  205.         $conversation->setLastMessageContent(substr($messageText0100));
  206.         $conversation->setLastMessageId($message->getId());
  207.         $conversation->setLastMessageAt(new \DateTime());
  208.         $conversation->setLastMessageSenderId($user->getId());  // ← NOUVEAU
  209.         $this->em->persist($conversation);
  210.         $this->em->flush();
  211.         // Vers le destinataire
  212.         $notification = new Notifications();
  213.         $notification->setViewed(false);
  214.         $notification->setOnlyAdmin(false);
  215.         $notification->setTitle("message");
  216.         if($conversation->getUserOne()->getLanguage() == "fr") {
  217.             $notification->setDescription("Message reçu par ".$emetteur->getName()." ".$emetteur->getLastname());
  218.             $this->notificationService->sendToUser(
  219.                 $destinataire,
  220.                 'Nouveau message',
  221.                 $this->cleanMessageNotification($message->getMessage()),
  222.                 ['type' => 'message''conversationId' => $conversation->getId()],
  223.             );
  224.         } else {
  225.             $this->notificationService->sendToUser(
  226.                 $destinataire,
  227.                 'New message',
  228.                 $this->cleanMessageNotification($message->getMessage()),
  229.                 ['type' => 'message''conversationId' => $conversation->getId()],
  230.             );
  231.             $notification->setDescription("Message received by ".$emetteur->getName()." ".$emetteur->getLastname());
  232.         }
  233.         $notification->setType("default");
  234.         $notification->setUser($conversation->getUserTwo());
  235.         $this->em->persist($notification);
  236.         $this->em->flush();
  237.         $fullname $user->getName()." ".$user->getLastname();
  238.         $avatar $this->getDefaultAvatar();
  239.         if ($user->getImage()) {
  240.             $avatar $user->getImageBase64();
  241.         }
  242.         try {
  243.             $client = new \GuzzleHttp\Client([
  244.                 'timeout' => 5.0,
  245.                 'verify' => false,
  246.             ]);
  247.             $websocketUrl $_ENV["WEBSOCKET_HTTP_URL"].'/broadcast';
  248.             error_log("╔═══════════════════════════════════════════════════════╗");
  249.             error_log("║  📡 BROADCAST WEBSOCKET - ENVOI MESSAGE              ║");
  250.             error_log("╚═══════════════════════════════════════════════════════╝");
  251.             error_log("🔡 [WebSocket] URL cible : " $websocketUrl);
  252.             error_log("🔡 [WebSocket] Group ID  : " $conversation->getId());
  253.             error_log("🔡 [WebSocket] Message ID: " $message->getId());
  254.             error_log("🔡 [WebSocket] User ID   : " $user->getId());
  255.             error_log("🔡 [WebSocket] Fullname  : " $fullname);
  256.             $broadcastData = [
  257.                 'groupId' => (int) $conversation->getId(),
  258.                 'message' => [
  259.                     'id' => (int) $message->getId(),
  260.                     'message' => $message->getMessage(),
  261.                     'isOwn' => false,
  262.                     'userId' => $user->getId(),
  263.                     'userName' => $fullname,
  264.                     'avatar' => $avatar,
  265.                     'createdAt' => $message->getCreatedAt()->format('c'),
  266.                 ]
  267.             ];
  268.             error_log("📤 [WebSocket] Données envoyées: " json_encode($broadcastData));
  269.             $response $client->post($websocketUrl, [
  270.                 'json' => $broadcastData,
  271.                 'headers' => [
  272.                     'Content-Type' => 'application/json',
  273.                 ]
  274.             ]);
  275.             $statusCode $response->getStatusCode();
  276.             $body $response->getBody()->getContents();
  277.             $bodyData json_decode($bodytrue);
  278.             error_log("✅ [WebSocket] Notification envoyée - Status: " $statusCode);
  279.             error_log("✅ [WebSocket] Réponse: " $body);
  280.             if (isset($bodyData['clientsNotified'])) {
  281.                 $clientsNotified $bodyData['clientsNotified'];
  282.                 error_log("📊 [WebSocket] Clients notifiés: " $clientsNotified);
  283.                 if ($clientsNotified === 0) {
  284.                     error_log("⚠️  [WebSocket] ATTENTION: Aucun client n'a reçu le message !");
  285.                     error_log("   Raisons possibles:");
  286.                     error_log("   1. Aucun navigateur connecté à cette conversation");
  287.                     error_log("   2. Les clients ne sont pas dans la bonne room");
  288.                     error_log("   3. Les clients n'ont pas appelé joinGroup()");
  289.                 }
  290.             }
  291.             error_log("═══════════════════════════════════════════════════════");
  292.         } catch (\GuzzleHttp\Exception\ConnectException $e) {
  293.             error_log("╔═══════════════════════════════════════════════════════╗");
  294.             error_log("║  ❌ ERREUR DE CONNEXION WEBSOCKET                     ║");
  295.             error_log("╚═══════════════════════════════════════════════════════╝");
  296.             error_log("❌ [WebSocket] Impossible de se connecter au serveur");
  297.             error_log("   URL tentée: " . ($websocketUrl ?? 'undefined'));
  298.             error_log("   Message: " $e->getMessage());
  299.             error_log("   ⚠️  Le serveur WebSocket est-il démarré ?");
  300.             error_log("   Commande: pm2 status");
  301.             error_log("═══════════════════════════════════════════════════════");
  302.         } catch (\GuzzleHttp\Exception\RequestException $e) {
  303.             error_log("╔═══════════════════════════════════════════════════════╗");
  304.             error_log("║  ❌ ERREUR REQUÊTE WEBSOCKET                          ║");
  305.             error_log("╚═══════════════════════════════════════════════════════╝");
  306.             error_log("❌ [WebSocket] Erreur lors de la requête");
  307.             error_log("   Message: " $e->getMessage());
  308.             if ($e->hasResponse()) {
  309.                 error_log("   Status: " $e->getResponse()->getStatusCode());
  310.                 error_log("   Body: " $e->getResponse()->getBody()->getContents());
  311.             }
  312.             error_log("═══════════════════════════════════════════════════════");
  313.         } catch (\Exception $e) {
  314.             error_log("╔═══════════════════════════════════════════════════════╗");
  315.             error_log("║  ❌ ERREUR GÉNÉRALE WEBSOCKET                         ║");
  316.             error_log("╚═══════════════════════════════════════════════════════╝");
  317.             error_log("❌ [WebSocket] Erreur: " $e->getMessage());
  318.             error_log("   Type: " get_class($e));
  319.             error_log("   File: " $e->getFile() . ":" $e->getLine());
  320.             error_log("⚠️  Le message a été enregistré mais la notification temps réel n'a pas pu être envoyée");
  321.             error_log("═══════════════════════════════════════════════════════");
  322.         }
  323.         return new JsonResponse([
  324.             'id' => (string)$message->getId(),
  325.             'message' => $message->getMessage(),
  326.             'isOwn' => true,
  327.             'userId' => $user->getId(),
  328.             'userName' => $fullname,
  329.             'avatar' => $avatar,
  330.             'createdAt' => $message->getCreatedAt() ? $message->getCreatedAt()->format('c') : null,
  331.         ], Response::HTTP_CREATED);
  332.     }
  333.     /**
  334.      * Récupérer les nouveaux messages depuis un certain ID (polling)
  335.      */
  336.     public function getNewMessages(Request $requestint $id): JsonResponse
  337.     {
  338.         $user $this->getUser();
  339.         $conversation $this->em->getRepository(Groups::class)->find($id);
  340.         if (!$conversation) {
  341.             return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  342.         }
  343.         if (!$conversation->hasUser($user)) {
  344.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  345.         }
  346.         $lastMessageId $request->query->get('lastMessageId'0);
  347.         $messages $this->em->getRepository(Messages::class)->findAfterMessage($conversation$lastMessageId);
  348.         $array = [];
  349.         foreach ($messages as $message) {
  350.             $isOwn $message->getUser()->getId() === $user->getId();
  351.             $fullname $message->getUser()->getName()." ".$message->getUser()->getLastname();
  352.             $avatar $this->getDefaultAvatar();
  353.             if ($message->getUser()->getImage()) {
  354.                 $avatar $message->getUser()->getImageBase64();
  355.             }
  356.             $array[] = [
  357.                 "id" => (string)$message->getId(),
  358.                 "message" => (string)$message->getMessage(),
  359.                 "isOwn" => $isOwn,
  360.                 "userId" => $message->getUser()->getId(),
  361.                 "userName" => $fullname,
  362.                 "avatar" => $avatar,
  363.                 "createdAt" => $message->getCreatedAt() ? $message->getCreatedAt()->format('c') : null,
  364.             ];
  365.         }
  366.         return new JsonResponse($array);
  367.     }
  368.     /**
  369.      * Rechercher dans les conversations
  370.      */
  371.     public function search(Request $request): JsonResponse
  372.     {
  373.         $user $this->getUser();
  374.         $searchTerm $request->query->get('q''');
  375.         if (empty(trim($searchTerm))) {
  376.             return new JsonResponse(['error' => 'Terme de recherche requis'], Response::HTTP_BAD_REQUEST);
  377.         }
  378.         $conversations $this->em->getRepository(Groups::class)->searchConversations($user$searchTerm);
  379.         $array = [];
  380.         foreach ($conversations as $conversation) {
  381.             $otherUser $conversation->getOtherUser($user);
  382.             if (!$otherUser) {
  383.                 continue;
  384.             }
  385.             $countMessages $this->em->getRepository(Messages::class)->countByConversation($conversation);
  386.             $fullname $otherUser->getName()." ".$otherUser->getLastname();
  387.             $avatar $this->getDefaultAvatar();
  388.             if ($otherUser->getImage()) {
  389.                 $avatar $otherUser->getImageBase64();
  390.             }
  391.             $array[] = [
  392.                 "id" => (string)$conversation->getId(),
  393.                 "conversationHash" => $conversation->getConversationHash(),
  394.                 "title" => $conversation->getTitle() ?: 'Conversation avec ' $fullname,
  395.                 "fullname" => $fullname,
  396.                 "userId" => $otherUser->getId(),
  397.                 "avatar" => $avatar,
  398.                 "count_messages" => $countMessages,
  399.                 "lastmessage" => $conversation->getLastMessageContent() ?? '',
  400.                 "lastMessageAt" => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  401.             ];
  402.         }
  403.         return new JsonResponse($array);
  404.     }
  405.     /**
  406.      * Supprimer une conversation (soft delete ou hard delete selon votre besoin)
  407.      */
  408.     public function deleteConversation(int $id): JsonResponse
  409.     {
  410.         $user $this->getUser();
  411.         $conversation $this->em->getRepository(Groups::class)->find($id);
  412.         if (!$conversation) {
  413.             return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  414.         }
  415.         if (!$conversation->hasUser($user)) {
  416.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  417.         }
  418.         return new JsonResponse(['message' => 'Conversation supprimée avec succès']);
  419.     }
  420.     /**
  421.      * Démarrer une conversation
  422.      */
  423.     public function startConversation(Request $requestint $userId): JsonResponse
  424.     {
  425.         $user $this->getUser();
  426.         if (!$user) {
  427.             return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  428.         }
  429.         $otherUser $this->em->getRepository(Users::class)->find($userId);
  430.         if (!$otherUser) {
  431.             return new JsonResponse(['error' => 'Utilisateur non trouvé'], Response::HTTP_NOT_FOUND);
  432.         }
  433.         if ($user->getId() === $otherUser->getId()) {
  434.             return new JsonResponse(['error' => 'Impossible de démarrer une conversation avec soi-même'], Response::HTTP_BAD_REQUEST);
  435.         }
  436.         $conversation $this->em->getRepository(Groups::class)->findOrCreateConversation($user$otherUser);
  437.         $fullname $otherUser->getName() . " " $otherUser->getLastname();
  438.         return new JsonResponse([
  439.             'id' => (string)$conversation->getId(),
  440.             'conversationHash' => $conversation->getConversationHash(),
  441.             'fullname' => $fullname,
  442.             'userId' => $otherUser->getId(),
  443.         ], Response::HTTP_CREATED);
  444.     }
  445.     // ========================================
  446.     // NOUVELLES MÉTHODES — V2 avec lu/non lu
  447.     // ========================================
  448.     /**
  449.      * Liste des conversations récentes V2 — avec lastMessageSenderId et unread_count
  450.      * Utilise le vrai compteur de messages non lus (basé sur readAt)
  451.      */
  452.     public function recentsV2(Request $request): JsonResponse
  453.     {
  454.         $user $this->getUser();
  455.         if (!$user) {
  456.             return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  457.         }
  458.         $limit $request->query->get('limit'20);
  459.         $conversations $this->em->getRepository(Groups::class)->recents($user$limit);
  460.         $messagesRepo $this->em->getRepository(Messages::class);
  461.         $array = [];
  462.         foreach ($conversations as $conversation) {
  463.             $otherUser $conversation->getOtherUser($user);
  464.             if (!$otherUser) {
  465.                 continue;
  466.             }
  467.             // Compteur de messages NON LUS (messages de l'autre que je n'ai pas lus)
  468.             $unreadCount $messagesRepo->countUnreadByConversation($conversation$user);
  469.             $fullname $otherUser->getName() . " " $otherUser->getLastname();
  470.             $avatar $this->getDefaultAvatar();
  471.             if ($otherUser->getImage()) {
  472.                 $avatar $otherUser->getImageBase64();
  473.             }
  474.             $array[] = [
  475.                 "id"                    => (string)$conversation->getId(),
  476.                 "conversationHash"      => $conversation->getConversationHash(),
  477.                 "title"                 => $conversation->getTitle() ?: 'Conversation avec ' $fullname,
  478.                 "fullname"              => $fullname,
  479.                 "userId"                => $otherUser->getId(),
  480.                 "userEmail"             => $otherUser->getEmail(),
  481.                 "avatar"                => $avatar,
  482.                 "count_messages"        => $unreadCount,                                    // ← maintenant = non lus uniquement
  483.                 "lastMessageSenderId"   => $conversation->getLastMessageSenderId(),         // ← NOUVEAU
  484.                 "open"                  => false,
  485.                 "lastmessage"           => $conversation->getLastMessageContent() ?? '',
  486.                 "lastMessageAt"         => $conversation->getLastMessageAt() ? $conversation->getLastMessageAt()->format('c') : null,
  487.                 "createdAt"             => $conversation->getCreatedAt() ? $conversation->getCreatedAt()->format('c') : null,
  488.             ];
  489.         }
  490.         return new JsonResponse($array);
  491.     }
  492.     /**
  493.      * Marquer tous les messages d'une conversation comme lus
  494.      * Appelé quand l'utilisateur ouvre une conversation
  495.      */
  496.     public function markAsRead(int $id): JsonResponse
  497.     {
  498.         $user $this->getUser();
  499.         if (!$user) {
  500.             return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  501.         }
  502.         $conversation $this->em->getRepository(Groups::class)->find($id);
  503.         if (!$conversation) {
  504.             return new JsonResponse(['error' => 'Conversation non trouvée'], Response::HTTP_NOT_FOUND);
  505.         }
  506.         if (!$conversation->hasUser($user)) {
  507.             return new JsonResponse(['error' => 'Accès interdit'], Response::HTTP_FORBIDDEN);
  508.         }
  509.         // Marquer comme lus tous les messages de l'autre utilisateur
  510.         $markedCount $this->em->getRepository(Messages::class)
  511.             ->markAsReadByConversation($conversation$user);
  512.         error_log("✅ [MarkAsRead] Conv {$id} — {$markedCount} messages marqués comme lus pour user {$user->getId()}");
  513.         return new JsonResponse([
  514.             'success'     => true,
  515.             'markedCount' => $markedCount,
  516.         ]);
  517.     }
  518.     /**
  519.      * Obtenir le nombre total de messages non lus (pour le badge de l'onglet Messages)
  520.      */
  521.     public function totalUnread(): JsonResponse
  522.     {
  523.         $user $this->getUser();
  524.         if (!$user) {
  525.             return new JsonResponse(['error' => 'Non authentifié'], Response::HTTP_UNAUTHORIZED);
  526.         }
  527.         $totalUnread $this->em->getRepository(Messages::class)
  528.             ->countTotalUnreadForUser($user);
  529.         return new JsonResponse([
  530.             'totalUnread' => $totalUnread,
  531.         ]);
  532.     }
  533.     // ========================================
  534.     // HELPERS PRIVÉS
  535.     // ========================================
  536.     private function cleanMessageNotification($texte$longueur 20) {
  537.         if (mb_strlen($texte) <= $longueur) {
  538.             return $texte;
  539.         }
  540.         return mb_substr($texte0$longueur) . '...';
  541.     }
  542.     /**
  543.      * Avatar SVG par défaut
  544.      */
  545.     private function getDefaultAvatar(): string
  546.     {
  547.         return "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojNjY3ZWVhO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiM3NjRiYTI7c3RvcC1vcGFjaXR5OjEiIC8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjEwMCIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCwgMTAwKSI+CiAgICA8Y2lyY2xlIGN4PSIwIiBjeT0iLTE1IiByPSIzNSIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgICA8cGF0aCBkPSJNIC01MCA2NSBRIC01MCAyNSAtMzUgMTAgUSAtMTUgLTUgMCAtNSBRIDE1IC01IDM1IDEwIFEgNTAgMjUgNTAgNjUgWiIgZmlsbD0iI2ZmZmZmZiIgb3BhY2l0eT0iMC45Ii8+CiAgPC9nPgo8L3N2Zz4K";
  548.     }
  549. }