Files
e-ticket/templates/account/edit_event.html.twig
Serreau Jovann 15616167d0 Add attestation system with digital signature, public verification, and detailed ticket listing
- Create Attestation entity with reference, signature hash (HMAC-SHA256), event, user, payload
- Add migration Version20260326180000 for attestation table
- Save each attestation in DB with unique signature for tamper-proof verification
- Add public route /attestation/ventes/r/{reference} for QR code verification (short URL)
- Keep fallback /attestation/ventes/{hash} route for base64-signed verification
- Public page shows "Attestation conforme" with signature proof, no detailed data
- QR code on PDF now uses short reference URL instead of full base64 hash (scannable)
- Increase QR code resolution to 300px for better readability
- Display verification URL on PDF next to QR code

Attestation PDF improvements:
- Rename "ATTESTATION DE VENTES" to "ATTESTATION"
- Add two modes: "Attestation detaillee" (with ticket list) and "Attestation simple" (certification only)
- Simple mode: certifies figures are valid, only paid billets/votes confirmed by Stripe count
- Detailed mode: adds full ticket listing with reference, order number, billet name, buyer name
- No amounts displayed in either mode
- Gold color scheme (#fabf04) for headers, borders, table headers, summary box
- Larger text in QR verification box for readability

Scanner: ROLE_ROOT buyer tickets always validate at scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:13:40 +01:00

841 lines
56 KiB
Twig

{% extends 'base.html.twig' %}
{% block title %}{{ event.title }} - E-Ticket{% endblock %}
{% block body %}
<div class="w-full md:w-[80%] mx-auto py-12 px-4">
<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">{{ event.title }}</h1>
<p class="font-bold text-gray-600 italic mb-4">Gestion de l'evenement.</p>
{% for message in app.flashes('success') %}
<div class="flash-success"><p class="font-black text-sm">{{ message }}</p></div>
{% endfor %}
{% for message in app.flashes('error') %}
<div class="flash-error"><p class="font-black text-sm">{{ message }}</p></div>
{% endfor %}
<div class="flex flex-wrap gap-4 mb-6">
{% if event.online %}
<form method="post" action="{{ path('app_account_toggle_event_online', {id: event.id}) }}">
<button type="submit" class="px-4 py-2 border-2 border-red-800 bg-red-600 text-white font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-red-800 transition-all">
Passer hors ligne
</button>
</form>
{% elseif owner.stripeChargesEnabled and owner.stripePayoutsEnabled %}
<form method="post" action="{{ path('app_account_toggle_event_online', {id: event.id}) }}">
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-green-500 text-white font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-green-700 transition-all">
Mettre en ligne
</button>
</form>
{% else %}
<span class="px-4 py-2 border-2 border-gray-300 bg-gray-100 text-gray-400 font-black uppercase text-xs tracking-widest cursor-not-allowed">
Stripe requis pour mettre en ligne
</span>
{% endif %}
{% if event.secret %}
<form method="post" action="{{ path('app_account_toggle_event_secret', {id: event.id}) }}">
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-yellow-500 transition-all">
Rendre public
</button>
</form>
{% else %}
<form method="post" action="{{ path('app_account_toggle_event_secret', {id: event.id}) }}">
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-gray-200 font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-gray-300 transition-all">
Rendre secret
</button>
</form>
{% endif %}
<div class="flex items-center gap-2 ml-auto">
{% 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 %}
{% if event.secret %}
<span class="badge-yellow text-xs font-black uppercase">Secret</span>
{% endif %}
</div>
</div>
{% if is_granted('ROLE_ROOT') %}
<div class="card-brutal mb-6">
<p class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-2">Statut Stripe du compte organisateur</p>
<div class="flex flex-wrap gap-3 items-center">
<span class="text-sm font-bold">{{ owner.companyName ?? (owner.firstName ~ ' ' ~ owner.lastName) }}</span>
{% if owner.stripeAccountId %}
<span class="badge-green text-xs font-black uppercase">Compte Stripe connecte</span>
{% else %}
<span class="badge-red text-xs font-black uppercase">Aucun compte Stripe</span>
{% endif %}
{% if owner.stripeChargesEnabled %}
<span class="badge-green text-xs font-black uppercase">Charges accepted</span>
{% else %}
<span class="badge-red text-xs font-black uppercase">Charges non accepted</span>
{% endif %}
{% if owner.stripePayoutsEnabled %}
<span class="badge-green text-xs font-black uppercase">Payouts accepted</span>
{% else %}
<span class="badge-red text-xs font-black uppercase">Payouts non accepted</span>
{% endif %}
</div>
</div>
{% endif %}
{% if event.online %}
<div class="card-brutal-green mb-6 flex flex-wrap items-center justify-between gap-4">
<div>
<p class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">URL publique</p>
<p class="text-sm font-mono font-bold break-all" id="event-url">{{ absolute_url(path('app_event_detail', {orgaSlug: event.account.slug, id: event.id, eventSlug: event.slug})) }}</p>
</div>
<button type="button" id="copy-url-btn" class="px-4 py-2 border-2 border-gray-900 bg-white font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-gray-100 transition-all">
Copier le lien
</button>
</div>
{% endif %}
{% set current_tab = app.request.query.get('tab', 'info') %}
<div class="flex flex-wrap overflow-x-auto mb-8">
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'info'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 border-r-0 {{ current_tab == 'info' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Informations</a>
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'categories'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 border-r-0 {{ current_tab == 'categories' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Categories</a>
{% if is_granted('ROLE_ROOT') or app.user.offer == 'custom' %}
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'billets'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 border-r-0 {{ current_tab == 'billets' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Billets</a>
{% endif %}
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'invitations'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 border-r-0 {{ current_tab == 'invitations' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Invitations</a>
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'tickets'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 border-r-0 {{ current_tab == 'tickets' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Tickets</a>
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'attestation'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 border-r-0 {{ current_tab == 'attestation' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Attestation</a>
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'stats'}) }}" class="flex-1 min-w-[100px] text-center py-3 border-3 border-gray-900 {{ current_tab == 'stats' ? 'bg-yellow-400' : 'bg-white' }} font-black uppercase text-xs tracking-widest transition-all">Statistiques</a>
</div>
{% if current_tab == 'info' %}
<div class="flex flex-col lg:flex-row gap-8">
<div class="flex-1 min-w-0">
<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">Changer l'affiche</label>
<input type="file" id="event_main_picture" name="event_main_picture" accept="image/png, image/jpeg, image/webp, image/gif" class="form-file">
</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>
<div class="w-full lg:w-[350px] flex-shrink-0">
<div class="card-brutal overflow-hidden sticky top-24">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Affiche</h2>
</div>
{% if event.eventMainPictureName %}
<img src="{{ ('/uploads/events/' ~ event.eventMainPictureName) | imagine_filter('medium') }}" alt="{{ event.title }}" class="w-full max-h-[350px] object-contain">
{% else %}
<div class="p-12 text-center bg-gray-50">
<div class="text-4xl mb-4 opacity-30">&#128247;</div>
<p class="text-gray-400 font-bold text-sm">Aucune affiche</p>
<p class="text-gray-300 text-xs font-bold mt-1">Ajoutez une image via le formulaire</p>
</div>
{% endif %}
</div>
</div>
</div>
{% elseif current_tab == 'categories' %}
<div class="card-brutal overflow-hidden mb-6">
<div class="section-header flex justify-between items-center">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Categories</h2>
</div>
<div class="p-6">
<form method="post" action="{{ path('app_account_event_add_category', {id: event.id}) }}" class="flex flex-wrap gap-3 items-end mb-6">
<div class="flex-1 min-w-[200px]">
<label for="cat_name" class="text-xs font-black uppercase tracking-widest form-label">Nouvelle categorie</label>
<input type="text" id="cat_name" name="name" required class="form-input focus:border-indigo-600" placeholder="Ex: PMR, VIP, Tribune...">
</div>
<div>
<label for="cat_start" class="text-xs font-black uppercase tracking-widest form-label">Debut vente</label>
<input type="datetime-local" id="cat_start" name="start_at" class="form-input focus:border-indigo-600" value="{{ "now"|date('Y-m-d\\TH:i') }}">
</div>
<div>
<label for="cat_end" class="text-xs font-black uppercase tracking-widest form-label">Fin vente</label>
<input type="datetime-local" id="cat_end" name="end_at" class="form-input focus:border-indigo-600" value="{{ event.endAt|date('Y-m-d\\TH:i') }}">
</div>
<button type="submit" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
+ Ajouter
</button>
</form>
{% if categories|length > 0 %}
<div id="categories-list" data-reorder-url="{{ path('app_account_event_reorder_categories', {id: event.id}) }}">
{% for category in categories %}
<div class="border-2 border-gray-900 mb-2 bg-white" data-id="{{ category.id }}">
<div class="flex flex-wrap items-center gap-4 p-4 cursor-move">
<span class="text-gray-400 cursor-grab">&#9776;</span>
<span class="font-black text-sm uppercase flex-1">{{ category.name }}</span>
<span class="text-xs font-bold text-gray-400">{{ category.startAt|date('d/m/Y H:i') }}{{ category.endAt|date('d/m/Y H:i') }}</span>
{% if category.hidden %}
<span class="badge-yellow text-xs font-black uppercase">Masquee</span>
{% elseif category.active %}
<span class="badge-green text-xs font-black uppercase">Active</span>
{% else %}
<span class="badge-red text-xs font-black uppercase">Inactive</span>
{% endif %}
<a href="{{ path('app_account_event_edit_category', {id: event.id, categoryId: category.id}) }}" class="px-2 py-1 border-2 border-gray-900 bg-white text-xs font-black uppercase hover:bg-gray-100 transition-all">&#9998;</a>
<form method="post" action="{{ path('app_account_event_delete_category', {id: event.id, categoryId: category.id}) }}" data-confirm="Supprimer cette categorie ?" class="inline">
<button type="submit" class="px-2 py-1 border-2 border-red-800 bg-red-600 text-white text-xs font-black uppercase cursor-pointer hover:bg-red-800 transition-all">&#10005;</button>
</form>
</div>
{% set category_billets = billets[category.id] ?? [] %}
{% if category_billets|length > 0 %}
<div class="border-t-2 border-gray-900 bg-gray-50 px-4 py-3 billets-list" data-reorder-url="{{ path('app_account_event_reorder_billets', {id: event.id}) }}">
{% for billet in category_billets %}
{% set sold = sold_counts[billet.id] ?? 0 %}
<div class="flex flex-wrap items-center gap-3 py-2 {{ not loop.last ? 'border-b border-gray-200' : '' }} cursor-move" data-billet-id="{{ billet.id }}">
<span class="text-gray-400 cursor-grab">&#9776;</span>
{% if billet.pictureName %}
<img src="{{ ('/uploads/billets/' ~ billet.pictureName) | imagine_filter('thumbnail') }}" alt="{{ billet.name }}" class="w-10 h-10 object-cover border border-gray-300">
{% endif %}
<span class="font-bold text-sm flex-1">{{ billet.name }}</span>
<span class="font-black text-sm text-indigo-600">{{ billet.priceHTDecimal|number_format(2, ',', ' ') }} &euro; HT</span>
<span class="text-[10px] font-bold text-gray-400">{{ billet.unlimited ? 'Illimite' : billet.quantity ~ ' places' }}</span>
<span class="text-[10px] font-bold text-gray-600">{{ sold }} vendu{{ sold > 1 ? 's' : '' }}</span>
{% if billet.generatedBillet %}
<span class="badge-green text-[10px] font-black uppercase">Billet</span>
{% else %}
<span class="badge-red text-[10px] font-black uppercase">Sans billet</span>
{% endif %}
{% if billet.definedExit %}
<span class="badge-yellow text-[10px] font-black uppercase">Sortie def.</span>
{% endif %}
{% if billet.notBuyable %}
<span class="badge-red text-[10px] font-black uppercase">Non achetable</span>
{% endif %}
{% if billet.type != 'billet' %}
<span class="badge-yellow text-[10px] font-black uppercase">{{ billet.type == 'reservation_brocante' ? 'Brocante' : 'Vote' }}</span>
{% endif %}
<a href="{{ path('app_account_event_edit_billet', {id: event.id, billetId: billet.id}) }}" class="px-2 py-1 border-2 border-gray-900 bg-white text-xs font-black uppercase hover:bg-gray-100 transition-all">&#9998;</a>
<form method="post" action="{{ path('app_account_event_delete_billet', {id: event.id, billetId: billet.id}) }}" data-confirm="Supprimer ce billet ?" class="inline">
<button type="submit" class="px-2 py-1 border-2 border-red-800 bg-red-600 text-white text-xs font-black uppercase cursor-pointer hover:bg-red-800 transition-all">&#10005;</button>
</form>
</div>
{% endfor %}
</div>
{% endif %}
<div class="border-t-2 border-gray-900 px-4 py-3 bg-gray-50">
<a href="{{ path('app_account_event_add_billet', {id: event.id, categoryId: category.id}) }}" class="inline-flex items-center gap-2 px-3 py-1 border-2 border-gray-900 bg-white text-xs font-black uppercase hover:bg-indigo-600 hover:text-white transition-all">
+ Ajouter un billet
</a>
</div>
</div>
{% endfor %}
</div>
{% set total_sold = 0 %}
{% set total_ht = 0 %}
{% set total_commission_eticket = 0 %}
{% set total_commission_stripe = 0 %}
{% for cat_billets in billets %}
{% for billet in cat_billets %}
{% set sold = sold_counts[billet.id] ?? 0 %}
{% set line_ht = billet.priceHTDecimal * sold %}
{% set eticket_fee = line_ht * (commission_rate / 100) %}
{% set stripe_fee = sold > 0 ? (line_ht * stripe_fee_rate) + ((stripe_fee_fixed / 100) * sold) : 0 %}
{% set total_sold = total_sold + sold %}
{% set total_ht = total_ht + line_ht %}
{% set total_commission_eticket = total_commission_eticket + eticket_fee %}
{% set total_commission_stripe = total_commission_stripe + stripe_fee %}
{% endfor %}
{% endfor %}
{% set total_commission = total_commission_eticket + total_commission_stripe %}
{% set total_net = total_ht - total_commission %}
<div class="card-brutal overflow-hidden mt-4">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Recapitulatif ventes (hors invitations)</h2>
</div>
<div class="p-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="border-2 border-gray-900 p-4 text-center">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Qt vendue</div>
<div class="text-2xl font-black">{{ total_sold }}</div>
</div>
<div class="border-2 border-gray-900 p-4 text-center">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Total HT</div>
<div class="text-2xl font-black text-indigo-600">{{ total_ht|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="border-2 border-gray-900 p-4 text-center">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Total commissions</div>
<div class="text-2xl font-black text-red-600">{{ total_commission|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="border-2 border-gray-900 p-4 text-center bg-green-50">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Total percu</div>
<div class="text-2xl font-black text-green-600">{{ total_net|number_format(2, ',', ' ') }} &euro;</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="border-2 border-gray-900 p-3">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Commission E-Ticket ({{ commission_rate }}%)</div>
<div class="text-lg font-black text-red-500">-{{ total_commission_eticket|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="border-2 border-gray-900 p-3">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Commission Stripe ({{ (stripe_fee_rate * 100)|number_format(1) }}% + {{ (stripe_fee_fixed / 100)|number_format(2, ',', ' ') }}&euro;/tx)</div>
<div class="text-lg font-black text-red-500">-{{ total_commission_stripe|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="border-2 border-gray-900 p-3 bg-gray-50">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Total commissions</div>
<div class="text-lg font-black text-red-600">-{{ total_commission|number_format(2, ',', ' ') }} &euro;</div>
</div>
</div>
</div>
</div>
{% else %}
<p class="text-gray-400 font-bold text-sm text-center py-8">Aucune categorie. Ajoutez-en une pour commencer a vendre des billets.</p>
{% endif %}
</div>
</div>
{% elseif current_tab == 'billets' and (is_granted('ROLE_ROOT') or app.user.offer == 'custom') %}
{% set bd = billet_design %}
<div class="flex flex-col lg:flex-row gap-6" id="billet-designer" data-preview-url="{{ path('app_account_event_billet_preview', {id: event.id}) }}" data-save-url="{{ path('app_account_event_save_billet_design', {id: event.id}) }}">
<div class="w-full lg:w-[350px] flex-shrink-0">
<div class="card-brutal overflow-hidden">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Personnalisation</h2>
</div>
<div class="p-6 space-y-4">
<div>
<label for="design_accent_color" class="text-xs font-black uppercase tracking-widest form-label">Couleur d'accent</label>
<input type="color" id="design_accent_color" name="accent_color" value="{{ bd ? bd.accentColor : '#4f46e5' }}" class="w-full h-10 border-2 border-gray-900 cursor-pointer">
</div>
<hr class="border-gray-300">
<div>
<label for="design_invitation_title" class="text-xs font-black uppercase tracking-widest form-label">Titre invitation</label>
<input type="text" id="design_invitation_title" name="invitation_title" value="{{ bd ? bd.invitationTitle : 'Invitation' }}" class="form-input focus:border-indigo-600">
</div>
<div>
<label for="design_invitation_color" class="text-xs font-black uppercase tracking-widest form-label">Couleur fond invitation</label>
<input type="color" id="design_invitation_color" name="invitation_color" value="{{ bd ? bd.invitationColor : '#d4a017' }}" class="w-full h-10 border-2 border-gray-900 cursor-pointer">
</div>
<hr class="border-gray-300">
<button type="button" id="billet-save-design" class="w-full btn-brutal font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
Sauvegarder le design
</button>
</div>
</div>
</div>
<div class="flex-1 min-w-0">
<div class="card-brutal overflow-hidden">
<div class="section-header flex justify-between items-center">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Apercu du billet — A4</h2>
<button type="button" id="billet-reload-preview" class="px-3 py-1 bg-white text-gray-900 text-[10px] font-black uppercase tracking-widest border-2 border-white hover:bg-gray-100 transition-all cursor-pointer">
&#8635; Recharger
</button>
</div>
<div class="bg-gray-100 p-4 overflow-x-auto">
<iframe id="billet-preview-frame" title="Apercu du billet" src="{{ path('app_account_event_billet_preview', {id: event.id}) }}" class="bg-white shadow-lg mx-auto block" style="width: 595px; height: 842px; border: 1px solid #ccc;"></iframe>
</div>
</div>
</div>
</div>
{% elseif current_tab == 'invitations' %}
<div class="card-brutal overflow-hidden mb-6">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Creer une invitation</h2>
</div>
<div class="p-6">
<form method="post" action="{{ path('app_account_event_create_invitation', {id: event.id}) }}" class="form-col">
<div class="form-row">
<div class="form-group">
<label for="inv_last_name" class="text-xs font-black uppercase tracking-widest form-label">Nom</label>
<input type="text" id="inv_last_name" name="last_name" required class="form-input focus:border-indigo-600" placeholder="Dupont">
</div>
<div class="form-group">
<label for="inv_first_name" class="text-xs font-black uppercase tracking-widest form-label">Prenom</label>
<input type="text" id="inv_first_name" name="first_name" required class="form-input focus:border-indigo-600" placeholder="Jean">
</div>
</div>
<div>
<label for="inv_email" class="text-xs font-black uppercase tracking-widest form-label">Email</label>
<input type="email" id="inv_email" name="email" required class="form-input focus:border-indigo-600" placeholder="jean.dupont@exemple.fr">
</div>
<div>
<p class="text-xs font-black uppercase tracking-widest form-label">Billets</p>
<div id="inv-items">
<div class="flex flex-wrap gap-3 items-end mb-3" data-inv-line>
<div class="flex-1 min-w-[200px]">
<select name="items[0][billet_id]" required class="form-input focus:border-indigo-600">
{% for cat_billets in billets %}
{% for billet in cat_billets %}
<option value="{{ billet.id }}">{{ billet.category.name }}{{ billet.name }}</option>
{% endfor %}
{% endfor %}
</select>
</div>
<div class="w-24">
<input type="number" name="items[0][quantity]" min="1" value="1" required class="form-input focus:border-indigo-600 text-center">
</div>
</div>
</div>
<button type="button" id="inv-add-line" class="text-xs font-black uppercase tracking-widest text-indigo-600 hover:text-indigo-800 transition-colors cursor-pointer">+ Ajouter un billet</button>
</div>
<div>
<button type="submit" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
Envoyer l'invitation
</button>
</div>
</form>
<script>
document.getElementById('inv-add-line').addEventListener('click', function() {
var container = document.getElementById('inv-items')
var index = container.querySelectorAll('[data-inv-line]').length
var first = container.querySelector('[data-inv-line]')
var clone = first.cloneNode(true)
var select = clone.querySelector('select')
select.name = 'items[' + index + '][billet_id]'
select.selectedIndex = 0
var input = clone.querySelector('input[type="number"]')
input.name = 'items[' + index + '][quantity]'
input.value = '1'
var removeBtn = document.createElement('button')
removeBtn.type = 'button'
removeBtn.className = 'px-2 py-1 border-2 border-red-800 bg-red-600 text-white text-xs font-black uppercase cursor-pointer hover:bg-red-800 transition-all'
removeBtn.textContent = '\u2715'
removeBtn.addEventListener('click', function() { clone.remove() })
clone.appendChild(removeBtn)
container.appendChild(clone)
})
</script>
</div>
</div>
<div class="card-brutal overflow-hidden mb-6">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Accreditation Staff / Exposant</h2>
</div>
<div class="p-6">
<form method="post" action="{{ path('app_account_event_create_accreditation', {id: event.id}) }}" class="form-col">
<div>
<label for="accred_type" class="text-xs font-black uppercase tracking-widest form-label">Type d'accreditation</label>
<select id="accred_type" name="accreditation_type" required class="form-input focus:border-indigo-600">
<option value="staff">Staff</option>
<option value="exposant">Exposant</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="accred_last_name" class="text-xs font-black uppercase tracking-widest form-label">Nom</label>
<input type="text" id="accred_last_name" name="last_name" required class="form-input focus:border-indigo-600" placeholder="Dupont">
</div>
<div class="form-group">
<label for="accred_first_name" class="text-xs font-black uppercase tracking-widest form-label">Prenom</label>
<input type="text" id="accred_first_name" name="first_name" required class="form-input focus:border-indigo-600" placeholder="Jean">
</div>
</div>
<div>
<label for="accred_email" class="text-xs font-black uppercase tracking-widest form-label">Email</label>
<input type="email" id="accred_email" name="email" required class="form-input focus:border-indigo-600" placeholder="jean.dupont@exemple.fr">
</div>
<div>
<button type="submit" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
Envoyer l'accreditation
</button>
</div>
</form>
</div>
</div>
{% if invitations|length > 0 %}
<div class="card-brutal overflow-hidden">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Invitations envoyees</h2>
</div>
<div class="p-6">
{% for order in invitations %}
<div class="flex flex-wrap items-center gap-4 py-3 {{ not loop.last ? 'border-b border-gray-200' : '' }}">
<div class="flex-1 min-w-0">
<p class="font-black text-sm">{{ order.firstName }} {{ order.lastName }}</p>
<p class="text-xs font-bold text-gray-400">{{ order.email }}</p>
</div>
<span class="text-xs font-bold text-gray-500">{{ order.orderNumber }}</span>
{% for item in order.items %}
<span class="text-xs font-bold text-gray-500">{{ item.billetName }} x{{ item.quantity }}</span>
{% endfor %}
<span class="text-xs font-bold text-gray-400">{{ order.createdAt|date('d/m/Y H:i') }}</span>
{% if order.status == 'paid' %}
<span class="badge-green text-[10px] font-black uppercase">Envoyee</span>
{% else %}
<span class="badge-yellow text-[10px] font-black uppercase">{{ order.status }}</span>
{% endif %}
<form method="post" action="{{ path('app_account_event_resend_invitation', {id: event.id, orderId: order.id}) }}">
<button type="submit" class="px-3 py-1 border-2 border-gray-900 bg-white text-xs font-black uppercase cursor-pointer hover:bg-indigo-600 hover:text-white transition-all">Renvoyer</button>
</form>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% elseif current_tab == 'tickets' %}
<div class="card-brutal mb-6">
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Rechercher un ticket</h2>
<form method="get" action="{{ path('app_account_edit_event', {id: event.id}) }}" class="flex flex-wrap gap-4 items-end">
<input type="hidden" name="tab" value="tickets">
<div class="flex-1 min-w-[200px]">
<label for="tickets-tq" class="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Recherche</label>
<input type="text" id="tickets-tq" name="tq" value="{{ tickets_search_query }}" class="form-input focus:border-indigo-600" placeholder="Reference, cle, nom, email...">
</div>
<button type="submit" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-white transition-all">Filtrer</button>
{% if tickets_search_query %}
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'tickets'}) }}" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-gray-100 transition-all">Effacer</a>
{% endif %}
</form>
</div>
<div class="card-brutal overflow-hidden">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Tickets vendus ({{ event_tickets.getTotalItemCount }})</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead class="bg-gray-100">
<tr>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500">Reference</th>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500">Cle</th>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500">Billet</th>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500">Acheteur</th>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500">Statut</th>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500">Scanne</th>
<th class="px-4 py-3 text-[10px] font-black uppercase tracking-widest text-gray-500 text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for ticket in event_tickets %}
<tr class="border-b border-gray-100 hover:bg-gray-50 transition-all">
<td class="px-4 py-3 text-xs font-mono font-bold">{{ ticket.reference }}</td>
<td class="px-4 py-3 text-xs font-mono text-gray-500">{{ ticket.securityKey }}</td>
<td class="px-4 py-3 text-sm font-bold">{{ ticket.billetName }}</td>
<td class="px-4 py-3">
<p class="text-sm font-bold">{{ ticket.billetBuyer.firstName }} {{ ticket.billetBuyer.lastName }}</p>
<p class="text-xs text-gray-400">{{ ticket.billetBuyer.email }}</p>
</td>
<td class="px-4 py-3">
{% if ticket.state == 'valid' %}
<span class="badge-green text-xs font-black uppercase">Valide</span>
{% elseif ticket.state == 'invalid' %}
<span class="badge-red text-xs font-black uppercase">Annule</span>
{% elseif ticket.state == 'expired' %}
<span class="badge-yellow text-xs font-black uppercase">Expire</span>
{% endif %}
{% if ticket.invitation %}
<span class="badge-indigo text-xs font-black uppercase ml-1">Invitation</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs text-gray-500">
{% if ticket.firstScannedAt %}
{{ ticket.firstScannedAt|date('d/m/Y H:i') }}
{% else %}
<span class="text-gray-300">-</span>
{% endif %}
</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
<a href="{{ path('app_account_event_download_ticket', {id: event.id, ticketId: ticket.id}) }}" target="_blank" class="inline-block text-[10px] font-black uppercase tracking-widest text-indigo-600 hover:text-indigo-800 transition-all mr-2" title="Telecharger">PDF</a>
<form method="post" action="{{ path('app_account_event_resend_ticket', {id: event.id, ticketId: ticket.id}) }}" class="inline">
<button type="submit" class="text-[10px] font-black uppercase tracking-widest text-blue-600 hover:text-blue-800 transition-all mr-2 cursor-pointer" title="Renvoyer par email">Renvoyer</button>
</form>
{% if ticket.state == 'valid' %}
<form method="post" action="{{ path('app_account_event_cancel_ticket', {id: event.id, ticketId: ticket.id}) }}" class="inline" onsubmit="return confirm('Annuler ce billet ?')">
<button type="submit" class="text-[10px] font-black uppercase tracking-widest text-red-600 hover:text-red-800 transition-all cursor-pointer" title="Annuler">Annuler</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="px-4 py-12 text-center text-gray-400 font-bold text-sm">Aucun ticket vendu.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if event_tickets.getTotalItemCount > 20 %}
<div class="flex justify-center gap-2 mt-6">
{% for page in 1..event_tickets.getPageCount %}
{% if page == event_tickets.getCurrentPageNumber %}
<span class="px-3 py-1 border-2 border-gray-900 bg-gray-900 text-white text-xs font-black">{{ page }}</span>
{% else %}
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'tickets', tp: page, tq: tickets_search_query}) }}" class="px-3 py-1 border-2 border-gray-900 text-xs font-black hover:bg-gray-100 transition-all">{{ page }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% elseif current_tab == 'attestation' %}
<div class="card-brutal overflow-hidden">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Generer une attestation de ventes</h2>
</div>
<div class="p-6">
<p class="text-sm text-gray-600 mb-6">Selectionnez les categories et/ou billets a inclure dans l'attestation. Le document PDF certifiera le nombre de billets vendus et le chiffre d'affaires HT (hors invitations).</p>
<form method="post" action="{{ path('app_account_event_attestation', {id: event.id}) }}" target="_blank">
{% if categories|length > 0 %}
<div class="mb-6">
<p class="text-xs font-black uppercase tracking-widest form-label mb-3">Categories</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
{% for category in categories %}
<label class="flex items-center gap-3 p-3 border-2 border-gray-200 hover:border-gray-900 transition-all cursor-pointer">
<input type="checkbox" name="categories[]" value="{{ category.id }}" class="w-4 h-4">
<span class="font-bold text-sm">{{ category.name }}</span>
{% if category.active %}
<span class="badge-green text-[10px] font-black uppercase ml-auto">Active</span>
{% endif %}
</label>
{% endfor %}
</div>
</div>
{% endif %}
{% set has_billets = false %}
{% for cat_billets in billets %}
{% if cat_billets|length > 0 %}
{% set has_billets = true %}
{% endif %}
{% endfor %}
{% if has_billets %}
<div class="mb-6">
<p class="text-xs font-black uppercase tracking-widest form-label mb-3">Billets individuels</p>
<p class="text-xs text-gray-400 mb-3">Si vous selectionnez une categorie ci-dessus, tous ses billets seront inclus. Utilisez cette section pour ajouter des billets specifiques en complement.</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
{% for category in categories %}
{% set cat_billets = billets[category.id] ?? [] %}
{% for billet in cat_billets %}
<label class="flex items-center gap-3 p-3 border-2 border-gray-200 hover:border-gray-900 transition-all cursor-pointer">
<input type="checkbox" name="billets[]" value="{{ billet.id }}" class="w-4 h-4">
<div class="flex-1 min-w-0">
<span class="font-bold text-sm block">{{ billet.name }}</span>
<span class="text-xs text-gray-400">{{ category.name }}{{ billet.priceHTDecimal|number_format(2, ',', ' ') }} &euro; HT — {{ sold_counts[billet.id] ?? 0 }} vendu{{ (sold_counts[billet.id] ?? 0) > 1 ? 's' : '' }}</span>
</div>
</label>
{% endfor %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="flex flex-wrap gap-3">
<button type="submit" name="mode" value="detail" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
Attestation detaillee
</button>
<button type="submit" name="mode" value="simple" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-[#fabf04] transition-all">
Attestation simple
</button>
<button type="button" onclick="this.closest('form').querySelectorAll('input[type=checkbox]').forEach(c => c.checked = true)" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-gray-100 transition-all">
Tout selectionner
</button>
<button type="button" onclick="this.closest('form').querySelectorAll('input[type=checkbox]').forEach(c => c.checked = false)" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-gray-100 transition-all">
Tout deselectionner
</button>
</div>
</form>
</div>
</div>
{% elseif current_tab == 'stats' %}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="card-brutal p-4 text-center">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Commandes</div>
<div class="text-2xl font-black">{{ event_total_orders }}</div>
</div>
<div class="card-brutal p-4 text-center">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Billets vendus</div>
<div class="text-2xl font-black">{{ event_total_sold }}</div>
</div>
<div class="card-brutal p-4 text-center">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Chiffre d'affaires HT</div>
<div class="text-2xl font-black text-indigo-600">{{ event_total_ht|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="card-brutal p-4 text-center bg-green-50">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Total percu</div>
{% set commission_eticket = event_total_ht * (commission_rate / 100) %}
{% set commission_stripe = event_total_orders > 0 ? (event_total_ht * stripe_fee_rate + event_total_orders * (stripe_fee_fixed / 100)) : 0 %}
{% set total_net = event_total_ht - commission_eticket - commission_stripe %}
<div class="text-2xl font-black text-green-600">{{ total_net|number_format(2, ',', ' ') }} &euro;</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="card-brutal p-4">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Commission E-Ticket ({{ commission_rate }}%)</div>
<div class="text-lg font-black text-red-500">-{{ commission_eticket|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="card-brutal p-4">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Commission Stripe ({{ (stripe_fee_rate * 100)|number_format(1) }}% + {{ (stripe_fee_fixed / 100)|number_format(2, ',', ' ') }}&euro;/tx)</div>
<div class="text-lg font-black text-red-500">-{{ commission_stripe|number_format(2, ',', ' ') }} &euro;</div>
</div>
<div class="card-brutal p-4 bg-gray-50">
<div class="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-1">Total commissions</div>
<div class="text-lg font-black text-red-600">-{{ (commission_eticket + commission_stripe)|number_format(2, ',', ' ') }} &euro;</div>
</div>
</div>
{% if billet_stats|length > 0 %}
<div class="card-brutal overflow-hidden mb-6">
<div class="section-header">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Ventes par billet</h2>
</div>
<div class="p-6">
{% for stat in billet_stats %}
<div class="flex flex-wrap items-center gap-4 py-3 {{ not loop.last ? 'border-b border-gray-200' : '' }}">
<span class="font-black uppercase text-sm flex-1">{{ stat.name }}</span>
<span class="text-sm font-bold text-gray-600">{{ stat.sold }} vendu{{ stat.sold > 1 ? 's' : '' }}</span>
<span class="font-black text-sm text-indigo-600">{{ (stat.revenue / 100)|number_format(2, ',', ' ') }} &euro; HT</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="card-brutal overflow-hidden">
<div class="section-header flex justify-between items-center">
<h2 class="text-[10px] font-black uppercase tracking-widest text-white">Commandes</h2>
</div>
<div class="p-6">
<form method="get" action="{{ path('app_account_edit_event', {id: event.id, tab: 'stats'}) }}" class="flex gap-3 mb-6">
<input type="hidden" name="tab" value="stats">
<input type="text" name="q" value="{{ search_query }}" placeholder="Rechercher par nom, email, numero..." class="form-input focus:border-indigo-600 flex-1">
<button type="submit" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-white transition-all">Rechercher</button>
{% if search_query %}
<a href="{{ path('app_account_edit_event', {id: event.id, tab: 'stats'}) }}" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-gray-100 transition-all">Reset</a>
{% endif %}
</form>
{% if event_orders|length > 0 %}
{% for order in event_orders %}
<div class="border-2 border-gray-900 bg-white mb-3 p-4">
<div class="flex flex-wrap items-center gap-3 mb-2">
<span class="font-black uppercase text-sm">{{ order.orderNumber }}</span>
{% if order.status == 'paid' %}
<span class="badge-green text-[10px] font-black uppercase">Payee</span>
{% elseif order.status == 'refunded' %}
<span class="badge-yellow text-[10px] font-black uppercase">Remboursee</span>
{% elseif order.status == 'cancelled' %}
<span class="badge-red text-[10px] font-black uppercase">Annulee</span>
{% else %}
<span class="badge-yellow text-[10px] font-black uppercase">En attente</span>
{% endif %}
<span class="text-xs font-bold text-gray-400">{{ order.createdAt|date('d/m/Y H:i') }}</span>
</div>
<div class="flex flex-wrap gap-4 text-sm font-bold text-gray-600 mb-2">
<span>{{ order.firstName }} {{ order.lastName }}</span>
<span class="text-gray-400">{{ order.email }}</span>
</div>
<div class="flex flex-wrap items-center gap-3 mb-2">
{% for item in order.items %}
<span class="text-xs font-bold text-gray-500">{{ item.billetName }} x{{ item.quantity }}</span>
{% endfor %}
<span class="font-black text-sm text-indigo-600">{{ order.totalHTDecimal|number_format(2, ',', ' ') }} &euro;</span>
{% if order.paymentMethod %}
<span class="text-xs text-gray-400 font-bold">{{ order.paymentMethod }}{% if order.cardBrand %} {{ order.cardBrand|upper }}{% endif %}{% if order.cardLast4 %} **** {{ order.cardLast4 }}{% endif %}</span>
{% endif %}
</div>
<div class="flex gap-2 mt-2">
<a href="{{ path('app_order_public', {orderNumber: order.orderNumber, token: order.accessToken}) }}" class="px-3 py-1 border-2 border-gray-900 bg-white text-xs font-black uppercase hover:bg-gray-100 transition-all" target="_blank">Voir</a>
{% if order.status == 'paid' %}
<form method="post" action="{{ path('app_account_event_cancel_order', {id: event.id, orderId: order.id}) }}" data-confirm="Annuler cette commande ? Les billets seront invalides.">
<button type="submit" class="px-3 py-1 border-2 border-red-800 bg-red-600 text-white text-xs font-black uppercase cursor-pointer hover:bg-red-800 transition-all">Annuler</button>
</form>
<form method="post" action="{{ path('app_account_event_refund_order', {id: event.id, orderId: order.id}) }}" data-confirm="Rembourser cette commande ? Le montant sera restitue au client.">
<button type="submit" class="px-3 py-1 border-2 border-gray-900 bg-[#fabf04] text-xs font-black uppercase cursor-pointer hover:bg-yellow-500 transition-all">Rembourser</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
<div class="mt-4">
{{ knp_pagination_render(event_orders) }}
</div>
{% else %}
<p class="text-gray-400 font-bold text-sm text-center py-8">{{ search_query ? 'Aucune commande trouvee.' : 'Aucune commande pour cet evenement.' }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}