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:
@@ -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
|
||||
|
||||
79
templates/account/edit_event.html.twig
Normal file
79
templates/account/edit_event.html.twig
Normal 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 %}
|
||||
@@ -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">
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user