- 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>
264 lines
10 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|