Files
e-ticket/src/Controller/HomeController.php
Serreau Jovann cca5575274 Add organizer invitation system: invite, accept, refuse
- OrganizerInvitation entity: companyName, firstName, lastName, email,
  message, status (sent/opened/accepted/refused), unique token (64 hex chars)
- Admin route /admin/organisateurs/inviter: form + invitation list with status
- Button "Inviter un organisateur" on admin organizers page
- Email with accept/refuse links using unique token
- Public route /invitation/{token}/{action}: accept or refuse without auth
- Response page: confirmation message for accept/refuse
- Migration, PHPStan config, 7 entity tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 17:41:31 +01:00

264 lines
10 KiB
PHP

<?php
namespace App\Controller;
use App\Entity\Billet;
use App\Entity\BilletBuyer;
use App\Entity\BilletOrder;
use App\Entity\Category;
use App\Entity\OrganizerInvitation;
use App\Entity\Event;
use App\Entity\User;
use App\Service\EventIndexService;
use App\Service\MailerService;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
private const BREADCRUMB_HOME = ['name' => 'Accueil', 'url' => '/'];
private const BREADCRUMB_ORGANIZERS = ['name' => 'Organisateurs', 'url' => '/organisateurs'];
#[Route('/', name: 'app_home')]
public function index(EntityManagerInterface $em): Response
{
$allUsers = $em->getRepository(User::class)->findAll();
$organizers = \count(array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && $u->isApproved()));
$eventsCount = \count($em->getRepository(Event::class)->findBy(['isOnline' => true, 'isSecret' => false]));
$ticketsSold = $em->getRepository(BilletOrder::class)->count([]);
$paidOrders = $em->getRepository(BilletBuyer::class)->findBy(['status' => BilletBuyer::STATUS_PAID]);
$totalHT = 0;
foreach ($paidOrders as $order) {
$totalHT += $order->getTotalHT();
}
return $this->render('home/index.html.twig', [
'breadcrumbs' => [
self::BREADCRUMB_HOME,
],
'stats' => [
'events' => $eventsCount,
'organizers' => $organizers,
'tickets' => $ticketsSold,
'totalHT' => $totalHT / 100,
'orders' => \count($paidOrders),
],
]);
}
#[Route('/evenements', name: 'app_events')]
public function events(Request $request, PaginatorInterface $paginator, EventIndexService $eventIndex): Response
{
$searchQuery = $request->query->getString('q', '');
$eventsQuery = $eventIndex->searchEvents('event_global', $searchQuery, ['isOnline' => true, 'isSecret' => false]);
$events = $paginator->paginate($eventsQuery, $request->query->getInt('page', 1), 12);
return $this->render('home/events.html.twig', [
'events' => $events,
'searchQuery' => $searchQuery,
'breadcrumbs' => [
self::BREADCRUMB_HOME,
['name' => 'Evenements', 'url' => '/evenements'],
],
]);
}
#[Route('/organisateurs', name: 'app_organizers')]
public function organizers(EntityManagerInterface $em): Response
{
$allUsers = $em->getRepository(User::class)->findAll();
$organizers = array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true) && $u->isApproved());
return $this->render('home/organizers.html.twig', [
'organizers' => $organizers,
'breadcrumbs' => [
self::BREADCRUMB_HOME,
self::BREADCRUMB_ORGANIZERS,
],
]);
}
#[Route('/organisateur/{id}-{slug}', name: 'app_organizer_detail', requirements: ['id' => '\d+', 'slug' => '[a-z0-9-]+'])]
public function organizerDetail(int $id, string $slug, EntityManagerInterface $em): Response
{
$organizer = $em->getRepository(User::class)->find($id);
if (!$organizer || !\in_array('ROLE_ORGANIZER', $organizer->getRoles(), true) || !$organizer->isApproved()) {
throw $this->createNotFoundException('Organisateur introuvable.');
}
if ($slug !== $organizer->getSlug()) {
return $this->redirectToRoute('app_organizer_detail', [
'id' => $organizer->getId(),
'slug' => $organizer->getSlug(),
], 301);
}
$events = $em->getRepository(Event::class)->findBy(
['account' => $organizer, 'isOnline' => true, 'isSecret' => false],
['startAt' => 'ASC'],
);
return $this->render('home/organizer_detail.html.twig', [
'organizer' => $organizer,
'events' => $events,
'breadcrumbs' => [
self::BREADCRUMB_HOME,
self::BREADCRUMB_ORGANIZERS,
['name' => $organizer->getCompanyName() ?? $organizer->getFirstName().' '.$organizer->getLastName(), 'url' => '/organisateur/'.$organizer->getId().'-'.$organizer->getSlug()],
],
]);
}
#[Route('/evenement/{orgaSlug}/{id}-{eventSlug}', name: 'app_event_detail', requirements: ['id' => '\d+', 'orgaSlug' => '[a-z0-9-]+', 'eventSlug' => '[a-z0-9-]+'])]
public function eventDetail(int $id, string $orgaSlug, string $eventSlug, EntityManagerInterface $em): Response
{
$event = $em->getRepository(Event::class)->find($id);
if (!$event || !$event->isOnline()) {
throw $this->createNotFoundException('Evenement introuvable.');
}
$organizer = $event->getAccount();
if ($orgaSlug !== $organizer->getSlug()) {
return $this->redirectToRoute('app_event_detail', [
'orgaSlug' => $organizer->getSlug(),
'id' => $event->getId(),
'eventSlug' => $event->getSlug(),
], 301);
}
if ($eventSlug !== $event->getSlug()) {
return $this->redirectToRoute('app_event_detail', [
'orgaSlug' => $organizer->getSlug(),
'id' => $event->getId(),
'eventSlug' => $event->getSlug(),
], 301);
}
$categories = $em->getRepository(Category::class)->findBy(
['event' => $event, 'isHidden' => false],
['position' => 'ASC'],
);
$billets = [];
foreach ($categories as $category) {
if (!$category->isActive()) {
continue;
}
$categoryBillets = $em->getRepository(Billet::class)->findBy(
['category' => $category],
['position' => 'ASC'],
);
$billets[$category->getId()] = array_filter($categoryBillets, fn (Billet $b) => !$b->isNotBuyable());
}
return $this->render('home/event_detail.html.twig', [
'event' => $event,
'organizer' => $organizer,
'categories' => $categories,
'billets' => $billets,
'breadcrumbs' => [
self::BREADCRUMB_HOME,
self::BREADCRUMB_ORGANIZERS,
['name' => $organizer->getCompanyName() ?? $organizer->getFirstName().' '.$organizer->getLastName(), 'url' => '/organisateur/'.$organizer->getId().'-'.$organizer->getSlug()],
['name' => $event->getTitle(), 'url' => '/evenement/'.$organizer->getSlug().'/'.$event->getId().'-'.$event->getSlug()],
],
]);
}
#[Route('/evenement/{id}/contact', name: 'app_event_contact', requirements: ['id' => '\d+'], methods: ['POST'])]
public function eventContact(int $id, Request $request, EntityManagerInterface $em, MailerService $mailerService): Response
{
$event = $em->getRepository(Event::class)->find($id);
if (!$event || !$event->isOnline()) {
throw $this->createNotFoundException('Evenement introuvable.');
}
$organizer = $event->getAccount();
$name = trim($request->request->getString('name'));
$firstname = trim($request->request->getString('firstname'));
$email = trim($request->request->getString('email'));
$message = trim($request->request->getString('message'));
if ('' === $name || '' === $email || '' === $message) {
$this->addFlash('error', 'Veuillez remplir tous les champs.');
return $this->redirectToRoute('app_event_detail', [
'orgaSlug' => $organizer->getSlug(),
'id' => $event->getId(),
'eventSlug' => $event->getSlug(),
]);
}
$html = $this->renderView('email/event_contact.html.twig', [
'event' => $event,
'senderName' => $firstname.' '.$name,
'senderEmail' => $email,
'senderMessage' => $message,
]);
$mailerService->sendEmail(
to: $organizer->getEmail(),
subject: sprintf('Message pour votre evenement "%s" - E-Ticket', $event->getTitle()),
content: $html,
replyTo: $email,
withUnsubscribe: false,
);
$this->addFlash('success', 'Votre message a ete envoye a l\'organisateur.');
return $this->redirectToRoute('app_event_detail', [
'orgaSlug' => $organizer->getSlug(),
'id' => $event->getId(),
'eventSlug' => $event->getSlug(),
]);
}
#[Route('/tarifs', name: 'app_tarifs')]
public function tarifs(): Response
{
return $this->render('home/tarifs.html.twig', [
'breadcrumbs' => [
self::BREADCRUMB_HOME,
['name' => 'Tarifs', 'url' => '/tarifs'],
],
]);
}
#[Route('/offline', name: 'app_offline_page')]
public function offline(): Response
{
return $this->render('home/offline.html.twig');
}
#[Route('/invitation/{token}/{action}', name: 'app_invitation_respond', requirements: ['action' => 'accept|refuse'], methods: ['GET'])]
public function respondInvitation(string $token, string $action, EntityManagerInterface $em): Response
{
$invitation = $em->getRepository(OrganizerInvitation::class)->findOneBy(['token' => $token]);
if (!$invitation || OrganizerInvitation::STATUS_SENT !== $invitation->getStatus()) {
throw $this->createNotFoundException();
}
if ('accept' === $action) {
$invitation->setStatus(OrganizerInvitation::STATUS_ACCEPTED);
} else {
$invitation->setStatus(OrganizerInvitation::STATUS_REFUSED);
}
$invitation->setRespondedAt(new \DateTimeImmutable());
$em->flush();
return $this->render('home/invitation_response.html.twig', [
'invitation' => $invitation,
'accepted' => 'accept' === $action,
]);
}
}