- 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>
841 lines
56 KiB
Twig
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">📷</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">☰</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">✎</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">✕</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">☰</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, ',', ' ') }} € 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">✎</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">✕</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, ',', ' ') }} €</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, ',', ' ') }} €</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, ',', ' ') }} €</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, ',', ' ') }} €</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, ',', ' ') }}€/tx)</div>
|
|
<div class="text-lg font-black text-red-500">-{{ total_commission_stripe|number_format(2, ',', ' ') }} €</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, ',', ' ') }} €</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">
|
|
↻ 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, ',', ' ') }} € 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, ',', ' ') }} €</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, ',', ' ') }} €</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, ',', ' ') }} €</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, ',', ' ') }}€/tx)</div>
|
|
<div class="text-lg font-black text-red-500">-{{ commission_stripe|number_format(2, ',', ' ') }} €</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, ',', ' ') }} €</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, ',', ' ') }} € 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, ',', ' ') }} €</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 %}
|