Files
e-ticket/templates/admin/organizers.html.twig
Serreau Jovann df7680d938 Add admin panel, Meilisearch buyer search, email redesign, and multiple features
Admin panel (/admin, ROLE_ROOT):
- Dashboard with CA HT Global/Commission cards and Meilisearch sync button
- Buyers page with search (Meilisearch), create form, pagination (KnpPaginator)
- Buyer actions: resend verification, force verify, reset password, delete
- Organizers page with tabs (pending/approved), approve/reject with emails
- Neo-brutalist design matching main site theme
- Vite admin entry point with dedicated SCSS
- CSP-compatible confirm dialogs via data-confirm attributes

Meilisearch integration:
- Auto-index buyers on email verification
- Remove from index on buyer deletion
- Manual sync button on dashboard
- Search bar on buyers page
- Add Meilisearch service to CI/SonarQube workflows
- Add MEILISEARCH env vars to .env.test
- Fix MeilisearchMessageHandler infinite loop: use request() directly instead
  of service methods that re-dispatch messages

Email templates:
- Redesign base email template to neo-brutalist style (borders, shadows, yellow footer)
- Add E-Cosplay logo, "E-Ticket solution proposee par e-cosplay.fr"
- Add admin_reset_password, organizer_approved, organizer_rejected templates

Other:
- Install knplabs/knp-paginator-bundle
- Add ^/admin access_control for ROLE_ROOT in security.yaml
- Update site footer with E-Ticket branding
- 18 admin tests, updated MeilisearchMessageHandler tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:07:07 +01:00

89 lines
6.3 KiB
Twig

{% extends 'admin/base.html.twig' %}
{% block title %}Organisateurs{% endblock %}
{% block body %}
<div style="margin-bottom:2rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Organisateurs</h1>
<p class="font-bold text-gray-500 italic">{{ organizers.getTotalItemCount }} organisateur{{ organizers.getTotalItemCount > 1 ? 's' : '' }}.</p>
</div>
<div style="display:flex;gap:0;margin-bottom:2rem;">
<a href="{{ path('app_admin_organizers', {tab: 'pending'}) }}" style="flex:1;text-align:center;padding:0.75rem;border:3px solid #111827;border-right:none;{{ tab == 'pending' ? 'background:#fabf04;' : 'background:white;' }}" class="font-black uppercase text-sm tracking-widest transition-all">En attente</a>
<a href="{{ path('app_admin_organizers', {tab: 'approved'}) }}" style="flex:1;text-align:center;padding:0.75rem;border:3px solid #111827;{{ tab == 'approved' ? 'background:#fabf04;' : 'background:white;' }}" class="font-black uppercase text-sm tracking-widest transition-all">Valides</a>
</div>
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#111827;">
<th style="padding:0.75rem 1.5rem;text-align:left;" class="text-[10px] font-black uppercase tracking-widest text-white">Organisateur</th>
<th style="padding:0.75rem 1.5rem;text-align:left;" class="text-[10px] font-black uppercase tracking-widest text-white">Raison sociale</th>
<th style="padding:0.75rem 1.5rem;text-align:left;" class="text-[10px] font-black uppercase tracking-widest text-white">SIRET</th>
<th style="padding:0.75rem 1.5rem;text-align:left;" class="text-[10px] font-black uppercase tracking-widest text-white">Ville</th>
<th style="padding:0.75rem 1.5rem;text-align:left;" class="text-[10px] font-black uppercase tracking-widest text-white">Offre</th>
<th style="padding:0.75rem 1.5rem;text-align:right;" class="text-[10px] font-black uppercase tracking-widest text-white">Actions</th>
</tr>
</thead>
<tbody>
{% for orga in organizers %}
<tr style="border-bottom:1px solid #e5e7eb;" class="hover:bg-gray-50 transition-all">
<td style="padding:0.75rem 1.5rem;">
<p class="font-bold text-sm">{{ orga.firstName }} {{ orga.lastName }}</p>
<p class="text-gray-400 text-xs">{{ orga.email }}</p>
</td>
<td style="padding:0.75rem 1.5rem;" class="text-sm text-gray-600">{{ orga.companyName }}</td>
<td style="padding:0.75rem 1.5rem;" class="text-sm text-gray-600 font-mono">{{ orga.siret }}</td>
<td style="padding:0.75rem 1.5rem;" class="text-sm text-gray-600">{{ orga.postalCode }} {{ orga.city }}</td>
<td style="padding:0.75rem 1.5rem;">
{% if orga.offer %}
<span style="background:#e0e7ff;border:2px solid #111827;padding:0.15rem 0.5rem;" class="text-xs font-black uppercase">{{ orga.offer }}</span>
{% else %}
<span class="text-gray-400 text-xs">—</span>
{% endif %}
</td>
<td style="padding:0.75rem 1.5rem;text-align:right;">
<div style="display:flex;gap:0.5rem;justify-content:flex-end;">
{% if not orga.approved %}
<form method="post" action="{{ path('app_admin_approve_organizer', {id: orga.id}) }}">
<button type="submit" style="border:2px solid #111827;padding:0.4rem 0.75rem;background:#fabf04;cursor:pointer;" class="text-xs font-black uppercase tracking-widest hover:bg-green-500 hover:text-black transition-all">Approuver</button>
</form>
<form method="post" action="{{ path('app_admin_reject_organizer', {id: orga.id}) }}" data-confirm="Etes-vous sur de vouloir refuser et supprimer le compte de {{ orga.firstName }} {{ orga.lastName }} ? Cette action est irreversible.">
<button type="submit" style="border:2px solid #991b1b;padding:0.4rem 0.75rem;background:#dc2626;color:white;cursor:pointer;" class="text-xs font-black uppercase tracking-widest hover:bg-red-800 transition-all">Refuser</button>
</form>
{% else %}
<form method="post" action="{{ path('app_admin_delete_buyer', {id: orga.id}) }}" data-confirm="Etes-vous sur de vouloir supprimer le compte de {{ orga.firstName }} {{ orga.lastName }} ({{ orga.email }}) ? Cette action est irreversible.">
<button type="submit" style="border:2px solid #991b1b;padding:0.4rem 0.75rem;background:#dc2626;color:white;cursor:pointer;" class="text-xs font-black uppercase tracking-widest hover:bg-red-800 transition-all">Supprimer</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" style="padding:3rem;text-align:center;" class="text-gray-400 font-bold text-sm">
{% if tab == 'pending' %}
Aucune demande en attente.
{% else %}
Aucun organisateur valide.
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if organizers.getTotalItemCount > 10 %}
<div style="display:flex;justify-content:center;gap:0.5rem;margin-top:1.5rem;">
{% for page in 1..organizers.getPageCount %}
{% if page == organizers.getCurrentPageNumber %}
<span style="border:2px solid #111827;padding:0.4rem 0.75rem;background:#fabf04;" class="text-xs font-black">{{ page }}</span>
{% else %}
<a href="{{ path('app_admin_organizers', {tab: tab, page: page}) }}" style="border:2px solid #111827;padding:0.4rem 0.75rem;background:white;" class="text-xs font-black hover:bg-gray-100 transition-all">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}