Files
e-ticket/templates/home/invitation_landing.html.twig

225 lines
16 KiB
Twig
Raw Normal View History

{% extends 'base.html.twig' %}
{% block title %}Invitation - {{ invitation.companyName }} - E-Ticket{% endblock %}
{% block body %}
<div class="bg-[#fbfbfb] overflow-x-hidden italic font-sans">
<section class="relative bg-white border-b-8 border-gray-900 px-4 pt-20 pb-16">
<div class="absolute inset-0 opacity-[0.03] pointer-events-none select-none overflow-hidden">
<span class="text-[8rem] md:text-[20rem] font-black uppercase leading-none block -rotate-12 translate-y-10">INVITATION</span>
</div>
<div class="max-w-3xl mx-auto relative z-10 text-center">
<div class="inline-block px-4 py-1 border-3 border-gray-900 bg-[#fabf04] font-black uppercase text-xs tracking-widest mb-6 shadow-[4px_4px_0px_rgba(0,0,0,1)]">Invitation organisateur</div>
<h1 class="text-4xl md:text-6xl font-black uppercase tracking-tighter leading-[0.85] mb-4">{{ invitation.companyName }}</h1>
<p class="text-lg font-bold text-gray-600">Bonjour {{ invitation.firstName }}, vous etes invite(e) a rejoindre <strong>E-Ticket</strong>.</p>
</div>
</section>
{% if invitation.offer or invitation.commissionRate is not null %}
<section class="bg-gray-900 text-white py-8 px-4">
<div class="max-w-3xl mx-auto text-center">
<p class="text-xs font-black uppercase tracking-widest text-[#fabf04] mb-2">Votre offre</p>
<p class="text-2xl font-black uppercase tracking-tighter">
{% if invitation.offer == 'free' %}Gratuit{% elseif invitation.offer == 'basic' %}Basic{% elseif invitation.offer == 'custom' %}Sur-mesure{% else %}{{ invitation.offer }}{% endif %}
</p>
{% if invitation.commissionRate is not null %}
<p class="text-sm font-bold text-gray-400 mt-1">Taux de commission E-Ticket : {{ invitation.commissionRate }}% <span class="text-gray-500">(hors frais Stripe)</span></p>
{% endif %}
</div>
</section>
{% endif %}
{% if invitation.message %}
<section class="py-8 px-4">
<div class="max-w-3xl mx-auto">
<div class="border-4 border-gray-900 bg-white p-6 shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-3">Message de l'equipe</p>
<p class="text-lg font-bold text-gray-700 leading-relaxed">{{ invitation.message }}</p>
</div>
</div>
</section>
{% endif %}
<section class="py-12 px-4">
<div class="max-w-3xl mx-auto">
<h2 class="text-3xl font-black uppercase tracking-tighter text-center mb-8">Decouvrir E-Ticket by E-Cosplay</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#127915;</div>
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Evenements</h3>
<p class="text-sm font-bold text-gray-600">Creez et gerez vos evenements en quelques clics. Billetterie, brocantes, votes.</p>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#128179;</div>
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Paiement securise</h3>
<p class="text-sm font-bold text-gray-600">Paiement en ligne via Stripe avec encaissement direct sur votre compte connect.</p>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#128200;</div>
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Statistiques</h3>
<p class="text-sm font-bold text-gray-600">Suivez vos ventes en temps reel, commandes, billets vendus et chiffre d'affaires.</p>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#127903;</div>
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Billets PDF</h3>
<p class="text-sm font-bold text-gray-600">Generez des billets PDF personnalises avec QR code, envoyes automatiquement par email.</p>
</div>
</div>
<h2 class="text-2xl font-black uppercase tracking-tighter text-center mb-6">Comment ca fonctionne ?</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div class="text-center">
<div class="w-12 h-12 border-4 border-gray-900 bg-[#fabf04] flex items-center justify-center mx-auto mb-3 font-black text-xl shadow-[4px_4px_0px_rgba(0,0,0,1)]">1</div>
<h3 class="font-black uppercase text-xs tracking-widest mb-2">Creez votre compte</h3>
<p class="text-xs font-bold text-gray-500">Configurez votre profil organisateur et connectez votre compte Stripe.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 border-4 border-gray-900 bg-[#fabf04] flex items-center justify-center mx-auto mb-3 font-black text-xl shadow-[4px_4px_0px_rgba(0,0,0,1)]">2</div>
<h3 class="font-black uppercase text-xs tracking-widest mb-2">Publiez vos evenements</h3>
<p class="text-xs font-bold text-gray-500">Ajoutez vos evenements, categories et billets. Mettez en ligne en un clic.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 border-4 border-gray-900 bg-[#fabf04] flex items-center justify-center mx-auto mb-3 font-black text-xl shadow-[4px_4px_0px_rgba(0,0,0,1)]">3</div>
<h3 class="font-black uppercase text-xs tracking-widest mb-2">Vendez et encaissez</h3>
<p class="text-xs font-bold text-gray-500">Les ventes arrivent directement sur votre compte Stripe. Billets generes automatiquement.</p>
</div>
</div>
<h2 class="text-2xl font-black uppercase tracking-tighter text-center mb-6">Les offres</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] {{ invitation.offer == 'free' ? 'ring-4 ring-[#fabf04]' : '' }}">
<h3 class="font-black uppercase text-sm tracking-widest mb-1">Gratuit</h3>
{% if invitation.offer == 'free' %}<span class="inline-block px-2 py-0.5 bg-[#fabf04] border-2 border-gray-900 text-[10px] font-black uppercase mb-3">Votre offre</span>{% endif %}
<ul class="space-y-2 text-xs font-bold text-gray-600 mt-3">
<li class="text-green-600">&#10003; 1 evenement</li>
<li class="text-green-600">&#10003; Billets standards</li>
<li class="text-green-600">&#10003; QR code</li>
<li class="text-gray-300">&#10005; Image par billet</li>
<li class="text-gray-300">&#10005; Design personnalise</li>
<li class="text-gray-300">&#10005; Generation PDF</li>
</ul>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] {{ invitation.offer == 'basic' ? 'ring-4 ring-[#fabf04]' : '' }}">
<h3 class="font-black uppercase text-sm tracking-widest mb-1">Basic</h3>
{% if invitation.offer == 'basic' %}<span class="inline-block px-2 py-0.5 bg-[#fabf04] border-2 border-gray-900 text-[10px] font-black uppercase mb-3">Votre offre</span>{% endif %}
<ul class="space-y-2 text-xs font-bold text-gray-600 mt-3">
<li class="text-green-600">&#10003; Evenements illimites</li>
<li class="text-green-600">&#10003; Billets standards</li>
<li class="text-green-600">&#10003; QR code</li>
<li class="text-green-600">&#10003; Generation PDF</li>
<li class="text-gray-300">&#10005; Image par billet</li>
<li class="text-gray-300">&#10005; Design personnalise</li>
</ul>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] {{ invitation.offer == 'custom' ? 'ring-4 ring-[#fabf04]' : '' }}">
<h3 class="font-black uppercase text-sm tracking-widest mb-1">Sur-mesure</h3>
{% if invitation.offer == 'custom' %}<span class="inline-block px-2 py-0.5 bg-[#fabf04] border-2 border-gray-900 text-[10px] font-black uppercase mb-3">Votre offre</span>{% endif %}
<ul class="space-y-2 text-xs font-bold text-gray-600 mt-3">
<li class="text-green-600">&#10003; Evenements illimites</li>
<li class="text-green-600">&#10003; Design personnalise</li>
<li class="text-green-600">&#10003; Image par billet</li>
<li class="text-green-600">&#10003; QR code</li>
<li class="text-green-600">&#10003; Generation PDF</li>
<li class="text-green-600">&#10003; Categories illimitees</li>
</ul>
</div>
</div>
<h2 class="text-2xl font-black uppercase tracking-tighter text-center mb-6">Commissions</h2>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="text-center">
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Commission E-Ticket</p>
<p class="text-4xl font-black text-indigo-600">{{ invitation.commissionRate ?? 3 }}%</p>
<p class="text-xs font-bold text-gray-500 mt-1">Par transaction sur le montant HT</p>
</div>
<div class="text-center">
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Commission Stripe</p>
<p class="text-4xl font-black text-gray-900">1.5% + 0.25 &euro;</p>
<p class="text-xs font-bold text-gray-500 mt-1">Frais standard cartes europeennes</p>
</div>
</div>
{% set rate = invitation.commissionRate ?? 3 %}
<div class="mt-6 pt-4 border-t-2 border-gray-200 overflow-x-auto">
<table class="w-full text-xs font-bold" style="min-width: 500px;">
<thead>
<tr class="border-b-2 border-gray-900">
<th class="py-2 text-left text-gray-400 uppercase tracking-widest">Prix billet</th>
<th class="py-2 text-right text-gray-400 uppercase tracking-widest">E-Ticket ({{ rate }}%)</th>
<th class="py-2 text-right text-gray-400 uppercase tracking-widest">Stripe</th>
<th class="py-2 text-right text-gray-400 uppercase tracking-widest">Total frais</th>
<th class="py-2 text-right text-green-600 uppercase tracking-widest">Vous recevez</th>
</tr>
</thead>
<tbody>
{% for price in [1, 2, 5, 10, 15, 20] %}
{% set eticket_fee = price * rate / 100 %}
{% set stripe_fee = price * 0.015 + 0.25 %}
{% set total_fee = eticket_fee + stripe_fee %}
{% set net = price - total_fee %}
<tr class="border-b border-gray-100">
<td class="py-2 text-left">{{ price|number_format(2, ',', ' ') }} &euro;</td>
<td class="py-2 text-right text-red-600">{{ eticket_fee|number_format(2, ',', ' ') }} &euro;</td>
<td class="py-2 text-right text-red-600">{{ stripe_fee|number_format(2, ',', ' ') }} &euro;</td>
<td class="py-2 text-right text-red-600">{{ total_fee|number_format(2, ',', ' ') }} &euro;</td>
<td class="py-2 text-right text-green-600 font-black">{{ net|number_format(2, ',', ' ') }} &euro;</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</section>
Complete TASK_CHECKUP: security, UX, tests, coverage, accessibility, config externalization Billetterie: - Partial refund support (STATUS_PARTIALLY_REFUNDED, refundedAmount field, migration) - Race condition fix: PESSIMISTIC_WRITE lock on stock decrement in transaction - Idempotency key on PaymentIntent::create, reuse existing PI if stripeSessionId set - Disable checkout when event ended (server 400 + template hide) - Webhook deduplication via cache (24h TTL on stripe event.id) - Email validation (filter_var) in OrderController guest flow - JSON cart validation (structure check before processing) - Invitation expiration after 7 days (isExpired method + landing page message) - Stripe Checkout fallback when JS fails to load (noscript + redirect) Config externalization: - Move Stripe fees (STRIPE_FEE_RATE, STRIPE_FEE_FIXED) and admin email (ADMIN_EMAIL) to .env/services.yaml - Replace all hardcoded contact@e-cosplay.fr across 13 files - MailerService: getAdminEmail()/getAdminFrom(), default $from=null resolves to admin UX & Accessibility: - ARIA tabs: role=tablist/tab/tabpanel, aria-selected, keyboard nav (arrows, Home, End) - aria-label on cart +/- buttons and editor toolbar buttons - tabindex=0 on editor toolbar buttons for keyboard access - data-confirm handler in app.js (was only in admin.js) - Cart error feedback on checkout failure - Billet designer save feedback (loading/success/error states) - Stock polling every 30s with rupture/low stock badges - Back to event link on payment page Security: - HTML sanitizer: BLOCKED_TAGS list (script, style, iframe, svg, etc.) - content fully removed - Stripe polling timeout (15s max) with fallback redirect - Rate limiting on public order access (20/5min) - .catch() on all fetch() calls (sortable, billet-designer) Tests (92% PHP, 100% JS lines): - PCOV added to dev Dockerfile - Test DB setup: .env.test with DATABASE_URL, Redis auth, Meilisearch key - Rate limiter disabled in test env - Makefile: test_db_setup, test_db_reset, run_test_php, run_test_coverage_php/js - New tests: InvitationFlowTest (21), AuditServiceTest (4), ExportServiceTest (9), InvoiceServiceTest (4) - New tests: SuspendedUserSubscriberTest, RateLimiterSubscriberTest, MeilisearchServiceTest - New tests: Stripe webhook payment_failed (6) + charge.refunded (6) - New tests: BilletBuyer refund, User suspended, OrganizerInvitation expiration - JS tests: stock polling (6), data-confirm (2), copy-url restore (1), editor ARIA (2), XSS (9), tabs keyboard (9) - ESLint + PHP CS Fixer: 0 errors - SonarQube exclusions aligned with vitest coverage config Infra: - Meilisearch consistency command (app:meilisearch:check-consistency --fix) + cron daily 3am - MeilisearchService: getAllDocumentIds(), listIndexes() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:14:06 +01:00
{% if expired|default(false) %}
<section class="py-12 px-4 border-t-8 border-gray-900 bg-white">
<div class="max-w-xl mx-auto text-center">
<div class="text-5xl mb-4 text-red-600">&#9201;</div>
<p class="font-black uppercase text-lg tracking-tighter">Invitation expiree</p>
<p class="text-sm font-bold text-gray-500 mt-2">Cette invitation a expire. Veuillez contacter l'administrateur pour en recevoir une nouvelle.</p>
</div>
</section>
{% elseif invitation.status in ['sent', 'opened'] %}
<section class="py-12 px-4 border-t-8 border-gray-900 bg-white">
<div class="max-w-xl mx-auto text-center">
<h2 class="text-2xl font-black uppercase tracking-tighter mb-6">Votre reponse</h2>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<form method="post" action="{{ path('app_invitation_respond', {token: invitation.token, action: 'accept'}) }}">
<button type="submit" class="w-full sm:w-auto px-8 py-4 border-4 border-gray-900 bg-[#fabf04] font-black uppercase text-sm tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-y-[-2px] transition-all cursor-pointer">
Accepter l'invitation
</button>
</form>
<form method="post" action="{{ path('app_invitation_respond', {token: invitation.token, action: 'refuse'}) }}">
<button type="submit" class="w-full sm:w-auto px-8 py-4 border-4 border-gray-900 bg-white font-black uppercase text-sm tracking-widest shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:bg-red-600 hover:text-white transition-all cursor-pointer">
Refuser
</button>
</form>
</div>
</div>
</section>
{% else %}
<section class="py-12 px-4 border-t-8 border-gray-900 bg-white">
<div class="max-w-xl mx-auto text-center">
{% if invitation.status == 'accepted' %}
<div class="text-5xl mb-4 text-green-600">&#10003;</div>
<p class="font-black uppercase text-lg tracking-tighter">Invitation acceptee</p>
<p class="text-sm font-bold text-gray-500 mt-2">Votre compte sera bientot active.</p>
{% elseif invitation.status == 'refused' %}
<div class="text-5xl mb-4 text-gray-400">&#10005;</div>
<p class="font-black uppercase text-lg tracking-tighter">Invitation refusee</p>
{% endif %}
</div>
</section>
{% endif %}
<section class="py-8 px-4 bg-gray-900 text-white text-center">
<p class="text-xs font-bold uppercase tracking-widest opacity-50">E-Ticket by E-Cosplay — Plateforme de billetterie en ligne</p>
</section>
</div>
{% endblock %}