Add event list with pagination, edit and delete actions in account page

- Display events table sorted by startAt ASC with status (en ligne/hors ligne)
- Add KnpPaginator for events (10 per page)
- Add edit event page (/mon-compte/evenement/{id}/modifier) with all fields + isOnline toggle
- Add delete event route (/mon-compte/evenement/{id}/supprimer) with confirmation
- Add Modifier/Supprimer buttons in events table
- Move Stripe warning outside the card
- Fix test to use fresh EntityManager for event assertion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-20 17:02:56 +01:00
parent 3c1b89ed32
commit 146a0d4da0
4 changed files with 211 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ use App\Service\MailerService;
use App\Service\PayoutPdfService;
use App\Service\StripeService;
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;
@@ -22,7 +23,7 @@ class AccountController extends AbstractController
private const BREADCRUMB_ACCOUNT = ['name' => 'Mon compte', 'url' => '/mon-compte'];
#[Route('/mon-compte', name: 'app_account')]
public function index(Request $request, StripeService $stripeService, EntityManagerInterface $em): Response
public function index(Request $request, StripeService $stripeService, EntityManagerInterface $em, PaginatorInterface $paginator): Response
{
/** @var User $user */
$user = $this->getUser();
@@ -43,6 +44,7 @@ class AccountController extends AbstractController
$payouts = [];
$subAccounts = [];
$events = [];
if ($isOrganizer) {
$payouts = $em->getRepository(Payout::class)->findBy(
['organizer' => $user],
@@ -52,6 +54,11 @@ class AccountController extends AbstractController
['parentOrganizer' => $user],
['createdAt' => 'DESC'],
);
$eventsQuery = $em->getRepository(\App\Entity\Event::class)->findBy(
['account' => $user],
['startAt' => 'ASC'],
);
$events = $paginator->paginate($eventsQuery, $request->query->getInt('page', 1), 10);
}
return $this->render('account/index.html.twig', [
@@ -59,6 +66,7 @@ class AccountController extends AbstractController
'isOrganizer' => $isOrganizer,
'payouts' => $payouts,
'subAccounts' => $subAccounts,
'events' => $events,
'breadcrumbs' => [
self::BREADCRUMB_HOME,
self::BREADCRUMB_ACCOUNT,
@@ -322,6 +330,68 @@ class AccountController extends AbstractController
]);
}
#[Route('/mon-compte/evenement/{id}/modifier', name: 'app_account_edit_event', methods: ['GET', 'POST'])]
public function editEvent(\App\Entity\Event $event, Request $request, EntityManagerInterface $em): Response
{
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
/** @var User $user */
$user = $this->getUser();
if ($event->getAccount()->getId() !== $user->getId()) {
throw $this->createAccessDeniedException();
}
if ($request->isMethod('POST')) {
$event->setTitle(trim($request->request->getString('title')));
$event->setDescription(trim($request->request->getString('description')) ?: null);
$event->setStartAt(new \DateTimeImmutable($request->request->getString('start_at')));
$event->setEndAt(new \DateTimeImmutable($request->request->getString('end_at')));
$event->setAddress(trim($request->request->getString('address')));
$event->setZipcode(trim($request->request->getString('zipcode')));
$event->setCity(trim($request->request->getString('city')));
$event->setIsOnline($request->request->getBoolean('is_online'));
$pictureFile = $request->files->get('event_main_picture');
if ($pictureFile) {
$event->setEventMainPictureFile($pictureFile);
}
$em->flush();
$this->addFlash('success', 'Evenement modifie avec succes.');
return $this->redirectToRoute('app_account', ['tab' => 'events']);
}
return $this->render('account/edit_event.html.twig', [
'event' => $event,
'breadcrumbs' => [
self::BREADCRUMB_HOME,
self::BREADCRUMB_ACCOUNT,
['name' => 'Modifier un evenement', 'url' => '/mon-compte/evenement/'.$event->getId().'/modifier'],
],
]);
}
#[Route('/mon-compte/evenement/{id}/supprimer', name: 'app_account_delete_event', methods: ['POST'])]
public function deleteEvent(\App\Entity\Event $event, EntityManagerInterface $em): Response
{
$this->denyAccessUnlessGranted('ROLE_ORGANIZER');
/** @var User $user */
$user = $this->getUser();
if ($event->getAccount()->getId() !== $user->getId()) {
throw $this->createAccessDeniedException();
}
$em->remove($event);
$em->flush();
$this->addFlash('success', 'Evenement supprime.');
return $this->redirectToRoute('app_account', ['tab' => 'events']);
}
/** @codeCoverageIgnore Test helper, not used in production */
#[Route('/mon-compte/test-payout', name: 'app_account_test_payout', methods: ['POST'])]
public function testPayout(EntityManagerInterface $em): Response

View File

@@ -0,0 +1,79 @@
{% extends 'base.html.twig' %}
{% block title %}Modifier un evenement - E-Ticket{% endblock %}
{% block body %}
<div class="page-container">
<a href="{{ path('app_account', {tab: 'events'}) }}" class="inline-flex items-center gap-2 text-sm font-black uppercase tracking-widest text-gray-500 hover:text-gray-900 transition-colors mb-8">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M15 19l-7-7 7-7"/></svg>
Retour aux evenements
</a>
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Modifier un evenement</h1>
<p class="font-bold text-gray-600 italic mb-8">Modifiez les informations de votre evenement.</p>
{% for message in app.flashes('error') %}
<div class="flash-error"><p class="font-black text-sm">{{ message }}</p></div>
{% endfor %}
<form method="post" action="{{ path('app_account_edit_event', {id: event.id}) }}" enctype="multipart/form-data" class="form-col">
<div>
<label for="event_title" class="text-xs font-black uppercase tracking-widest form-label">Titre de l'evenement</label>
<input type="text" id="event_title" name="title" required class="form-input focus:border-indigo-600" value="{{ event.title }}">
</div>
<div>
<label for="event_description" class="text-xs font-black uppercase tracking-widest form-label">Description</label>
<e-ticket-editor>
<textarea id="event_description" name="description" rows="5" placeholder="Decrivez votre evenement...">{{ event.description }}</textarea>
</e-ticket-editor>
</div>
<div class="form-row">
<div class="form-group">
<label for="event_start_at" class="text-xs font-black uppercase tracking-widest form-label">Date et heure de debut</label>
<input type="datetime-local" id="event_start_at" name="start_at" required class="form-input focus:border-indigo-600" value="{{ event.startAt|date('Y-m-d\\TH:i') }}">
</div>
<div class="form-group">
<label for="event_end_at" class="text-xs font-black uppercase tracking-widest form-label">Date et heure de fin</label>
<input type="datetime-local" id="event_end_at" name="end_at" required class="form-input focus:border-indigo-600" value="{{ event.endAt|date('Y-m-d\\TH:i') }}">
</div>
</div>
<div>
<label for="event_address" class="text-xs font-black uppercase tracking-widest form-label">Adresse</label>
<input type="text" id="event_address" name="address" required class="form-input focus:border-indigo-600" value="{{ event.address }}">
</div>
<div class="form-row">
<div class="flex-1 min-w-[120px] max-w-[200px]">
<label for="event_zipcode" class="text-xs font-black uppercase tracking-widest form-label">Code postal</label>
<input type="text" id="event_zipcode" name="zipcode" required maxlength="10" class="form-input focus:border-indigo-600" value="{{ event.zipcode }}">
</div>
<div class="flex-[2] min-w-[200px]">
<label for="event_city" class="text-xs font-black uppercase tracking-widest form-label">Ville</label>
<input type="text" id="event_city" name="city" required class="form-input focus:border-indigo-600" value="{{ event.city }}">
</div>
</div>
<div>
<label for="event_main_picture" class="text-xs font-black uppercase tracking-widest form-label">Image principale</label>
{% if event.eventMainPictureName %}
<p class="text-xs font-bold text-gray-400 mb-2">Image actuelle : {{ event.eventMainPictureName }}</p>
{% endif %}
<input type="file" id="event_main_picture" name="event_main_picture" accept="image/*" class="form-file">
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="event_is_online" name="is_online" value="1" class="w-5 h-5 border-2 border-gray-900 cursor-pointer" {{ event.online ? 'checked' : '' }}>
<label for="event_is_online" class="text-sm font-black uppercase tracking-widest cursor-pointer">Mettre en ligne</label>
</div>
<div>
<button type="submit" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
Enregistrer les modifications
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -126,20 +126,73 @@
</a>
</div>
<div class="card-brutal">
{% if isOrganizer and (not app.user.stripeChargesEnabled or not app.user.stripePayoutsEnabled) %}
<div class="card-brutal-warn mb-6">
<p class="font-black text-sm">Configuration Stripe ou validation est requise !</p>
</div>
{% endif %}
<div class="card-brutal overflow-hidden">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Mes evenements / Brocantes / Reservations</h2>
</div>
{% if isOrganizer and (not app.user.stripeChargesEnabled or not app.user.stripePayoutsEnabled) %}
<div class="p-6 bg-amber-100 border-b-2 border-gray-200">
<p class="font-black text-sm">Configuration Stripe ou validation est requise !</p>
</div>
{% endif %}
{% if events|length > 0 %}
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-50 border-b-2 border-gray-200">
<th class="px-6 py-3 text-left text-[10px] font-black uppercase tracking-widest text-gray-400">Evenement</th>
<th class="px-6 py-3 text-left text-[10px] font-black uppercase tracking-widest text-gray-400">Date</th>
<th class="px-6 py-3 text-left text-[10px] font-black uppercase tracking-widest text-gray-400">Lieu</th>
<th class="px-6 py-3 text-center text-[10px] font-black uppercase tracking-widest text-gray-400">Statut</th>
<th class="px-6 py-3 text-right text-[10px] font-black uppercase tracking-widest text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr class="border-b border-gray-200 hover:bg-gray-50 transition-colors">
<td class="px-6 py-4">
<p class="font-black text-sm">{{ event.title }}</p>
</td>
<td class="px-6 py-4">
<p class="text-sm font-bold">{{ event.startAt|date('d/m/Y') }}</p>
<p class="text-xs text-gray-400 font-bold">{{ event.startAt|date('H:i') }} - {{ event.endAt|date('H:i') }}</p>
</td>
<td class="px-6 py-4">
<p class="text-sm font-bold">{{ event.city }}</p>
<p class="text-xs text-gray-400 font-bold">{{ event.zipcode }}</p>
</td>
<td class="px-6 py-4 text-center">
{% if event.online %}
<span class="badge-green text-xs font-black uppercase">En ligne</span>
{% else %}
<span class="badge-red text-xs font-black uppercase">Hors ligne</span>
{% endif %}
</td>
<td class="px-6 py-4 text-right">
<div class="flex gap-2 justify-end">
<a href="{{ path('app_account_edit_event', {id: event.id}) }}" class="px-3 py-1 border-2 border-gray-900 bg-white text-xs font-black uppercase tracking-widest hover:bg-gray-100 transition-all">Modifier</a>
<form method="post" action="{{ path('app_account_delete_event', {id: event.id}) }}" data-confirm="Etes-vous sur de vouloir supprimer cet evenement ?" class="inline">
<button type="submit" class="px-3 py-1 border-2 border-red-800 bg-red-600 text-white text-xs font-black uppercase tracking-widest hover:bg-red-800 transition-all cursor-pointer">Supprimer</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-12 text-center">
<p class="text-gray-400 font-bold text-sm">Aucun evenement pour le moment.</p>
</div>
{% endif %}
</div>
{% if events.getTotalItemCount > 10 %}
<div class="mt-6">
{{ knp_pagination_render(events) }}
</div>
{% endif %}
{% elseif tab == 'subaccounts' %}
<div class="card-brutal p-6 mb-8">

View File

@@ -508,9 +508,9 @@ class AccountControllerTest extends WebTestCase
self::assertResponseRedirects('/mon-compte?tab=events');
$event = $em->getRepository(\App\Entity\Event::class)->findOneBy(['title' => 'Convention Test']);
$freshEm = static::getContainer()->get(EntityManagerInterface::class);
$event = $freshEm->getRepository(\App\Entity\Event::class)->findOneBy(['title' => 'Convention Test']);
self::assertNotNull($event);
self::assertSame($user->getId(), $event->getAccount()->getId());
self::assertSame('Un super evenement', $event->getDescription());
self::assertSame('Beautor', $event->getCity());
}