<?php
namespace App\Controller;
use App\Entity\ChatFormation;
use App\Entity\FormationElearning;
use App\Form\ChatFormationType;
use App\Repository\ChatFormationRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\FormationElearningRepository;
use App\Repository\SettingRepository;
use App\Repository\UserRepository;
use DateTimeZone;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
#[Route('/chat/formation')]
class ChatFormationController extends AbstractController
{
#[Route('/chat/unread-count', name: 'app_chat_unread_count', methods: ['GET'])]
public function getUnreadCount(
EntityManagerInterface $em,
): JsonResponse {
$user = $this->getUser();
if (!$user) {
return new JsonResponse(['count' => 0]);
}
$roles = $user->getRoles();
if (in_array('ROLE_ADMIN', $roles)) {
// Admin voit les messages non lus envoyés par les students
$count = $em->createQueryBuilder()
->select('COUNT(c.id)')
->from(ChatFormation::class, 'c')
->where('c.isRead = false OR c.isRead IS NULL')
->andWhere('c.senderRole = :role')
->setParameter('role', 'student')
->getQuery()
->getSingleScalarResult();
} elseif (in_array('ROLE_STUDENT', $roles)) {
// Student voit les messages admin non lus
// dans les formations où il a lui-même participé
$subQb = $em->createQueryBuilder()
->select('IDENTITY(sub.formation)')
->from(ChatFormation::class, 'sub')
->where('sub.sender = :user')
->setParameter('user', $user);
$count = $em->createQueryBuilder()
->select('COUNT(c.id)')
->from(ChatFormation::class, 'c')
->where('c.isRead = false OR c.isRead IS NULL')
->andWhere('c.senderRole = :role')
->andWhere(
$em->createQueryBuilder()->expr()->in(
'IDENTITY(c.formation)',
$subQb->getDQL()
)
)
->setParameter('role', 'admin')
->setParameter('user', $user)
->getQuery()
->getSingleScalarResult();
} else {
$count = 0;
}
return new JsonResponse(['count' => (int)$count]);
}
#[Route('/chat/recent-messages', name: 'app_chat_recent_messages', methods: ['POST'])]
public function getRecentMessages(EntityManagerInterface $em): JsonResponse
{
$user = $this->getUser();
if (!$user) {
return new JsonResponse([]);
}
$roles = $user->getRoles();
if (in_array('ROLE_ADMIN', $roles)) {
$messages = $em->createQueryBuilder()
->select('c', 'u', 'f')
->from(ChatFormation::class, 'c')
->join('c.sender', 'u')
->join('c.formation', 'f')
->where('c.senderRole = :role')
->andWhere('c.isRead = false OR c.isRead IS NULL')
->andWhere('c.parentMessage IS NULL')
->setParameter('role', 'student')
->orderBy('c.isRead', 'ASC')
->addOrderBy('c.createdAt', 'DESC')
->getQuery()
->getResult();
} elseif (in_array('ROLE_STUDENT', $roles)) {
$formationIds = $em->createQueryBuilder()
->select('IDENTITY(sub.formation)')
->from(ChatFormation::class, 'sub')
->where('sub.sender = :user')
->setParameter('user', $user)
->getQuery()
->getSingleColumnResult();
if (empty($formationIds)) {
return new JsonResponse([]);
}
$messages = $em->createQueryBuilder()
->select('c', 'u', 'f')
->from(ChatFormation::class, 'c')
->join('c.sender', 'u')
->join('c.formation', 'f')
->where('c.senderRole = :role')
->andWhere('c.formation IN (:formations)')
->andWhere('c.isRead = false OR c.isRead IS NULL')
->setParameter('role', 'admin')
->setParameter('formations', $formationIds)
->orderBy('c.isRead', 'ASC')
->addOrderBy('c.createdAt', 'DESC')
->getQuery()
->getResult();
} else {
return new JsonResponse([]);
}
// === Filtrer pour ne garder qu'un message par (sender + formation) ===
$filtered = [];
$seen = [];
foreach ($messages as $msg) {
$key = $msg->getSender()?->getId() . '_' . $msg->getFormation()?->getId();
if (!isset($seen[$key])) {
$seen[$key] = true;
$filtered[] = $msg;
}
}
// Limiter à 10 résultats après filtrage
$filtered = array_slice($filtered, 0, 10);
$data = [];
foreach ($filtered as $msg) {
$data[] = [
'id' => $msg->getId(),
'message' => mb_substr(strip_tags($msg->getMessage()), 0, 80),
'studentId' => $msg->getSender()?->getId(),
'prenom' => $msg->getSender()?->getPrenom(),
'nom' => $msg->getSender()?->getNom(),
'formation' => $msg->getFormation()?->getTitre(),
'formationSlug' => $msg->getFormation()?->getSlug(),
'formationId' => $msg->getFormation()?->getId(),
'isRead' => $msg->isRead(),
'role' => in_array('ROLE_ADMIN', $roles) ? 'admin' : 'student',
'datee' => $msg->getCreatedAt()?->format('d/m/Y H:i'),
];
}
return new JsonResponse($data);
}
#[Route('/{id}', name: 'app_chat_formation', methods: ['GET'])]
#[Security("is_granted('ROLE_STUDENT') or is_granted('ROLE_ADMIN')")]
public function index(
int $id,
FormationElearningRepository $formationRepo,
ChatFormationRepository $chatRepo,
SettingRepository $settingRepo,
EntityManagerInterface $entityManager
): Response {
$formation = $formationRepo->find($id);
if (!$formation) {
throw $this->createNotFoundException('Formation introuvable.');
}
$user = $this->getUser();
// Récupère les messages du thread
$messages = $chatRepo->createQueryBuilder('c')
->where('c.formation = :formation')
->andWhere('(c.sender = :user)
OR (c.senderRole = :adminRole AND c.parentMessage IN (
SELECT m FROM App\Entity\ChatFormation m
WHERE m.sender = :user AND m.formation = :formation
))')
->setParameter('formation', $formation)
->setParameter('user', $user)
->setParameter('adminRole', 'admin')
->orderBy('c.createdAt', 'ASC')
->getQuery()
->getResult();
// Marque les messages non-lus comme lus
foreach ($messages as $msg) {
if ($msg->getSenderRole() === 'admin' && !$msg->isRead()) {
$msg->setIsRead(true);
}
}
// On applique les changements en base de données (Uniquement les updates ici)
$entityManager->flush();
return $this->render('chat_formation/chat.html.twig', [
'formation' => $formation,
'messages' => $messages,
'setting' => $settingRepo->find(1),
]);
}
/**
* Envoyer un message (AJAX POST) – côté stagiaire.
* URL : /chat/formation/{id}/send
*/
#[Route('/{id}/send', name: 'app_chat_formation_send', methods: ['POST'])]
#[Security("is_granted('ROLE_STUDENT') or is_granted('ROLE_ADMIN')")]
public function send(
int $id,
Request $request,
FormationElearningRepository $formationRepo,
ChatFormationRepository $chatRepo,
EntityManagerInterface $em
): JsonResponse {
$formation = $formationRepo->find($id);
if (!$formation) {
return $this->json(['success' => false, 'message' => 'Formation introuvable.'], 404);
}
$data = json_decode($request->getContent(), true);
$message = trim($data['message'] ?? '');
if (empty($message)) {
return $this->json(['success' => false, 'message' => 'Message vide.'], 400);
}
$user = $this->getUser();
$chat = new ChatFormation();
$chat->setMessage($message);
$chat->setSender($user);
$chat->setFormation($formation);
$chat->setCreatedAt(new \DateTime('now', new DateTimeZone('Africa/Tunis')));
$chat->setIsRead(false);
$chat->setSenderRole(in_array('ROLE_ADMIN', $user->getRoles()) ? 'admin' : 'student');
$em->persist($chat);
$em->flush();
return $this->json([
'success' => true,
'id' => $chat->getId(),
'message' => $chat->getMessage(),
'senderName' => $user->getPrenom() . ' ' . $user->getNom(),
'senderRole' => $chat->getSenderRole(),
'avatar' => $user->getLogo(),
'createdAt' => $chat->getCreatedAt()->format('d/m/Y H:i'),
]);
}
/**
* Récupère les nouveaux messages (polling AJAX).
* URL : /chat/formation/{id}/poll?after={lastId}
*/
#[Route('/{id}/poll', name: 'app_chat_formation_poll', methods: ['GET'])]
#[Security("is_granted('ROLE_STUDENT') or is_granted('ROLE_ADMIN')")]
public function poll(
int $id,
Request $request,
FormationElearningRepository $formationRepo,
ChatFormationRepository $chatRepo,
EntityManagerInterface $em
): JsonResponse {
$formation = $formationRepo->find($id);
if (!$formation) {
return $this->json([]);
}
$lastId = (int) $request->query->get('after', 0);
$user = $this->getUser();
$newMessages = $chatRepo->createQueryBuilder('c')
->where('c.formation = :formation')
->andWhere('c.id > :lastId')
->andWhere('c.sender = :user OR c.senderRole = :adminRole')
->setParameter('formation', $formation)
->setParameter('lastId', $lastId)
->setParameter('user', $user)
->setParameter('adminRole', 'admin')
->orderBy('c.createdAt', 'ASC')
->getQuery()
->getResult();
$result = [];
foreach ($newMessages as $msg) {
if ($msg->getSenderRole() === 'admin' && !$msg->isRead()) {
$msg->setIsRead(true);
}
$result[] = [
'id' => $msg->getId(),
'message' => $msg->getMessage(),
'senderName' => $msg->getSender()->getPrenom() . ' ' . $msg->getSender()->getNom(),
'senderRole' => $msg->getSenderRole(),
'avatar' => $msg->getSender()->getLogo(),
'createdAt' => $msg->getCreatedAt()->format('d/m/Y H:i'),
];
}
$em->flush();
return $this->json($result);
}
// ─────────────────────────────────────────────────────────────────
// ADMIN : liste des conversations par formation
// ─────────────────────────────────────────────────────────────────
/**
* Liste toutes les formations ayant des messages (admin).
* URL : /chat/formation/admin/list
*/
// 1. LA LISTE DES DISCUSSIONS
#[Route('/admin/list', name: 'app_chat_admin_list', methods: ['GET'])]
#[Security("is_granted('ROLE_ADMIN')")]
public function adminList(SettingRepository $settingRepo, ChatFormationRepository $chatRepo): Response
{
return $this->render('chat_formation/admin_list.html.twig', [
'threads' => $chatRepo->findAllThreadsForAdmin(), // Utilise une méthode propre au repository
'setting' => $settingRepo->find(1),
]);
}
// 2. LA VUE DU CHAT DÉTAILLÉE (C'est ici qu'on définit 'student')
#[Route('/admin/chat/{formationId}/{studentId}', name: 'app_chat_admin_view', methods: ['GET'])]
public function adminView(
int $formationId,
int $studentId,
FormationElearningRepository $fRepo,
UserRepository $uRepo,
ChatFormationRepository $chatRepo,
EntityManagerInterface $entityManager,
Request $request
): Response {
$formation = $fRepo->find($formationId);
$student = $uRepo->find($studentId);
if (!$formation || !$student) throw $this->createNotFoundException();
$messages = $chatRepo->findAllMessagesForThread($formation, $student);
foreach ($messages as $message) {
if ($message->getSenderRole() === 'student' && !$message->isRead()) {
$message->setIsRead(true);
}
$entityManager->flush();
}
// Si c'est une requête AJAX (clic à gauche), on rend un template partiel
if ($request->isXmlHttpRequest()) {
return $this->render('chat_formation/_admin_chat_content.html.twig', [
'formation' => $formation,
'student' => $student,
'messages' => $messages,
]);
}
// Sinon on rend la page complète (premier chargement)
return $this->render('chat_formation/admin_list.html.twig', [
'formation' => $formation,
'student' => $student,
'messages' => $messages,
'threads' => $chatRepo->findAllThreadsForAdmin(),
]);
}
/**
* Liste des stagiaires ayant écrit pour une formation (admin).
* URL : /chat/formation/admin/{id}/students
*/
#[Route('/admin/{id}/students', name: 'app_chat_admin_students', methods: ['GET'])]
#[Security("is_granted('ROLE_ADMIN')")]
public function adminStudents(
int $id,
FormationElearningRepository $formationRepo,
ChatFormationRepository $chatRepo,
UserRepository $userRepo,
SettingRepository $settingRepo
): Response {
$formation = $formationRepo->find($id);
if (!$formation) {
throw $this->createNotFoundException();
}
// Récupère les IDs distincts des stagiaires ayant envoyé un message
$rows = $chatRepo->createQueryBuilder('c')
->select('IDENTITY(c.sender) as sid')
->where('c.formation = :f')
->andWhere('c.senderRole = :role')
->setParameter('f', $formation)
->setParameter('role', 'student')
->groupBy('c.sender')
->getQuery()
->getArrayResult();
$students = [];
foreach ($rows as $row) {
$student = $userRepo->find($row['sid']);
if ($student) {
$unread = $chatRepo->createQueryBuilder('c')
->select('COUNT(c.id)')
->where('c.formation = :f')
->andWhere('c.sender = :s')
->andWhere('c.isRead = false')
->andWhere('c.senderRole = :role')
->setParameter('f', $formation)
->setParameter('s', $student)
->setParameter('role', 'student')
->getQuery()
->getSingleScalarResult();
$students[] = ['student' => $student, 'unread' => (int) $unread];
}
}
return $this->render('chat_formation/adminStudents.html.twig', [
'formation' => $formation,
'students' => $students,
'setting' => $settingRepo->find(1),
]);
}
/**
* Thread admin ↔ stagiaire pour une formation.
* URL : /chat/formation/admin/{formationId}/thread/{studentId}
*/
#[Route('/admin/{formationId}/thread/{studentId}', name: 'app_chat_admin_thread', methods: ['GET'])]
#[Security("is_granted('ROLE_ADMIN')")]
public function adminThread(
int $formationId,
int $studentId,
FormationElearningRepository $formationRepo,
ChatFormationRepository $chatRepo,
UserRepository $userRepo,
SettingRepository $settingRepo,
EntityManagerInterface $em
): Response {
$formation = $formationRepo->find($formationId);
$student = $userRepo->find($studentId);
if (!$formation || !$student) {
throw $this->createNotFoundException();
}
// Récupère tous les messages de ce thread
$messages = $chatRepo->createQueryBuilder('c')
->where('c.formation = :f')
->andWhere('c.sender = :s OR (c.senderRole = :adminRole AND c.parentMessage IS NOT NULL)')
->setParameter('f', $formation)
->setParameter('s', $student)
->setParameter('adminRole', 'admin')
->orderBy('c.createdAt', 'ASC')
->getQuery()
->getResult();
// Marque les messages du stagiaire comme lus
foreach ($messages as $msg) {
if ($msg->getSenderRole() === 'student' && !$msg->isRead()) {
$msg->setIsRead(true);
}
}
$em->flush();
return $this->render('chat_formation/adminThread.html.twig', [
'formation' => $formation,
'student' => $student,
'messages' => $messages,
'setting' => $settingRepo->find(1),
]);
}
#[Route('/admin/chatlist/formation/{id}', name: 'app_chat_formation_admin_list')]
public function adminList2(
SettingRepository $settingRepository,
FormationElearning $formation,
ChatFormationRepository $chatRepo,
Request $request,
UserRepository $userRepo,
EntityManagerInterface $em
): Response {
$studentId = $request->query->get('studentId'); // optionnel
$conversations = $chatRepo->findLatestConversationsByFormation($formation);
$student = null;
$messages = null;
if ($studentId) {
$student = $userRepo->find($studentId);
if ($student) {
$messages = $chatRepo->findAllMessagesForThread($formation, $student);
// Marquer les messages comme lus
foreach ($messages as $msg) {
if ($msg->getSenderRole() === 'student' && !$msg->isRead()) {
$msg->setIsRead(true);
}
}
// Sauvegarde en base
$em->flush();
}
}
return $this->render('chat_formation/admin_list.html.twig', [
'formation' => $formation,
'threads' => $conversations,
'student' => $student,
'messages' => $messages,
'setting' => $settingRepository->find(1),
]);
}
/**
* Admin répond à un message (AJAX POST).
* URL : /chat/formation/admin/{formationId}/reply/{studentId}
*/
#[Route('/admin/reply/{formationId}/{studentId}', name: 'app_chat_admin_reply', methods: ['POST'])]
public function adminReply(
int $formationId,
int $studentId,
Request $request,
EntityManagerInterface $em,
FormationElearningRepository $fRepo,
UserRepository $uRepo,
ChatFormationRepository $chatRepo
): JsonResponse {
$data = json_decode($request->getContent(), true);
$messageContent = $data['message'] ?? '';
if (empty(trim($messageContent))) {
return $this->json(['success' => false, 'error' => 'Message vide'], 400);
}
$formation = $fRepo->find($formationId);
$student = $uRepo->find($studentId);
// On récupère le dernier message du stagiaire pour le lier (parentMessage)
$lastStudentMsg = $chatRepo->findOneBy(
['formation' => $formation, 'sender' => $student, 'senderRole' => 'student'],
['createdAt' => 'DESC']
);
$chat = new ChatFormation();
$chat->setMessage($messageContent);
$chat->setFormation($formation);
$chat->setSender($this->getUser()); // Admin connecté
$chat->setSenderRole('admin');
$chat->setCreatedAt(new \DateTime('now', new \DateTimeZone('Africa/Tunis')));
$chat->setParentMessage($lastStudentMsg);
$chat->setIsRead(false);
$em->persist($chat);
$em->flush();
return $this->json([
'success' => true,
'id' => $chat->getId(),
'message' => $chat->getMessage(),
'senderName' => 'Vous',
'createdAt' => $chat->getCreatedAt()->format('H:i')
]);
}
/**
* Polling admin : nouveaux messages du stagiaire.
* URL : /chat/formation/admin/{formationId}/poll/{studentId}?after={lastId}
*/
#[Route('/admin/{formationId}/poll/{studentId}', name: 'app_chat_admin_poll', methods: ['GET'])]
#[Security("is_granted('ROLE_ADMIN')")]
public function adminPoll(
int $formationId,
int $studentId,
Request $request,
FormationElearningRepository $formationRepo,
ChatFormationRepository $chatRepo,
UserRepository $userRepo,
EntityManagerInterface $em
): JsonResponse {
$formation = $formationRepo->find($formationId);
$student = $userRepo->find($studentId);
if (!$formation || !$student) return $this->json([]);
$lastId = (int) $request->query->get('after', 0);
$newMessages = $chatRepo->createQueryBuilder('c')
->where('c.formation = :f')
->andWhere('c.id > :lastId')
->andWhere('c.sender = :s OR c.senderRole = :adminRole')
->setParameter('f', $formation)
->setParameter('lastId', $lastId)
->setParameter('s', $student)
->setParameter('adminRole', 'admin')
->orderBy('c.createdAt', 'ASC')
->getQuery()
->getResult();
$result = [];
foreach ($newMessages as $msg) {
if ($msg->getSenderRole() === 'student' && !$msg->isRead()) {
$msg->setIsRead(true);
}
$result[] = [
'id' => $msg->getId(),
'message' => $msg->getMessage(),
'senderName' => $msg->getSender()->getPrenom() . ' ' . $msg->getSender()->getNom(),
'senderRole' => $msg->getSenderRole(),
'avatar' => $msg->getSender()->getLogo(),
'createdAt' => $msg->getCreatedAt()->format('d/m/Y H:i'),
];
}
$em->flush();
return $this->json($result);
}
}