- BilletOrder entity: individual tickets with unique ETICKET-XXXX reference,
billetBuyer link, billet link, isScanned, scannedAt for entry control
- BilletOrderService: generates tickets after payment, creates A4 PDF with
BilletDesign colors if present (default otherwise), real QR code via
endroid/qr-code, event poster + org logo as base64, sends confirmation
email with all ticket PDFs attached
- PDF template (pdf/billet.html.twig): A4 layout matching preview design,
real QR code linking to /ticket/verify/{reference}
- Email template: order recap table, ticket references list, link to
/ma-commande/{reference}
- Public order page /ma-commande/{reference}: no auth required, shows
order details, ticket list with individual PDF download links
- Ticket verification page /ticket/verify/{reference}: shows valid/scanned
status with ticket and event details
- Download route /ma-commande/{ref}/billet/{ticketRef}: generates PDF on-the-fly
- Migration for billet_order table with unique reference index
- BilletOrderTest: 8 tests, 24 assertions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
8.6 KiB
Twig
197 lines
8.6 KiB
Twig
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
{% set ac = design ? design.accentColor : '#4f46e5' %}
|
|
{% set inv_color = design ? design.invitationColor : '#d4a017' %}
|
|
{% set inv_title = design ? design.invitationTitle : 'Invitation' %}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@page { size: A4; margin: 0; }
|
|
body {
|
|
width: 595px;
|
|
height: 842px;
|
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
|
background: #fff;
|
|
color: #111;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ticket {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.zone-top {
|
|
flex: 1;
|
|
display: flex;
|
|
min-height: 0;
|
|
}
|
|
|
|
.zone-hg {
|
|
flex: 1;
|
|
padding: 28px 20px 20px 32px;
|
|
border-right: 3px solid {{ ac }};
|
|
border-bottom: 3px solid {{ ac }};
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.zone-hd {
|
|
width: 220px;
|
|
flex-shrink: 0;
|
|
border-bottom: 3px solid {{ ac }};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #fafafa;
|
|
overflow: hidden;
|
|
}
|
|
.zone-hd img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
}
|
|
|
|
.zone-bottom {
|
|
padding: 16px 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
background: {{ ac }};
|
|
color: #fff;
|
|
}
|
|
|
|
.event-title { font-size: 20px; font-weight: 900; text-transform: uppercase; letter-spacing: -0.5px; line-height: 1.15; margin-bottom: 3px; }
|
|
.event-badge { font-size: 8px; font-weight: 700; color: {{ ac }}; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 14px; }
|
|
|
|
.info-row { display: flex; align-items: baseline; padding: 4px 0; border-bottom: 1px solid #11111110; }
|
|
.info-row:last-child { border-bottom: none; }
|
|
.info-label { width: 65px; flex-shrink: 0; font-size: 7px; font-weight: 900; text-transform: uppercase; letter-spacing: 1.5px; opacity: 0.4; }
|
|
.info-value { font-size: 11px; font-weight: 700; }
|
|
|
|
.separator { height: 3px; background: {{ ac }}; margin: 12px 0; }
|
|
|
|
.billet-name { font-size: 15px; font-weight: 900; text-transform: uppercase; margin-bottom: 2px; }
|
|
.billet-price { font-size: 20px; font-weight: 900; color: {{ ac }}; margin-bottom: 10px; }
|
|
|
|
.meta-grid { display: flex; gap: 20px; flex-wrap: wrap; }
|
|
.meta-label { font-size: 7px; font-weight: 900; text-transform: uppercase; letter-spacing: 1.5px; opacity: 0.4; }
|
|
.meta-value { font-size: 10px; font-weight: 700; margin-bottom: 4px; }
|
|
|
|
.billet-badges { display: flex; gap: 6px; margin-top: 10px; }
|
|
.exit-badge { padding: 4px 8px; font-size: 8px; font-weight: 900; text-transform: uppercase; letter-spacing: 1px; border: 2px solid; }
|
|
.exit-definitive { background: #fee2e2; color: #991b1b; border-color: #991b1b; }
|
|
.exit-libre { background: #dcfce7; color: #166534; border-color: #166534; }
|
|
.invitation-badge { padding: 4px 8px; font-size: 8px; font-weight: 900; text-transform: uppercase; letter-spacing: 2px; color: #fff; }
|
|
|
|
.qr-section { margin-top: auto; display: flex; align-items: flex-end; justify-content: space-between; padding-top: 10px; border-top: 1px solid #11111112; }
|
|
.qr-box { width: 120px; height: 120px; display: flex; align-items: center; justify-content: center; background: #fff; }
|
|
.qr-box img { width: 120px; height: 120px; }
|
|
.ref-block { text-align: right; }
|
|
.ref-label { font-size: 7px; font-weight: 900; text-transform: uppercase; letter-spacing: 1.5px; opacity: 0.35; }
|
|
.ref-value { font-size: 9px; font-weight: 700; font-family: monospace; opacity: 0.55; }
|
|
|
|
.org-logo { width: 45px; height: 45px; object-fit: contain; flex-shrink: 0; }
|
|
.org-name { font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: 1.5px; }
|
|
.org-details { font-size: 8px; font-weight: 600; opacity: 0.7; margin-top: 2px; }
|
|
.powered { font-size: 6px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; opacity: 0.4; margin-left: auto; text-align: right; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="ticket">
|
|
<div class="zone-top">
|
|
<div class="zone-hg">
|
|
<div class="event-title">{{ event.title }}</div>
|
|
<div class="event-badge">Billet d'entree</div>
|
|
|
|
<div class="info-row">
|
|
<div class="info-label">Date</div>
|
|
<div class="info-value">{{ event.startAt|date('d/m/Y') }}</div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-label">Horaires</div>
|
|
<div class="info-value">{{ event.startAt|date('H:i') }} — {{ event.endAt|date('H:i') }}</div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-label">Lieu</div>
|
|
<div class="info-value">{{ event.address }}</div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-label">Ville</div>
|
|
<div class="info-value">{{ event.zipcode }} {{ event.city }}</div>
|
|
</div>
|
|
|
|
<div class="separator"></div>
|
|
|
|
<div class="billet-name">{{ ticket.billetName }}</div>
|
|
<div class="billet-price">{{ ticket.unitPriceHTDecimal|number_format(2, ',', ' ') }} € HT</div>
|
|
|
|
<div class="meta-grid">
|
|
<div>
|
|
<div class="meta-label">Categorie</div>
|
|
<div class="meta-value">{{ ticket.billet.category.name }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="meta-label">Date d'achat</div>
|
|
<div class="meta-value">{{ order.paidAt ? order.paidAt|date('d/m/Y H:i') : order.createdAt|date('d/m/Y H:i') }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="meta-label">Acheteur</div>
|
|
<div class="meta-value">{{ order.firstName }} {{ order.lastName }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="meta-label">E-mail</div>
|
|
<div class="meta-value">{{ order.email }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="billet-badges">
|
|
{% if ticket.billet.definedExit %}
|
|
<div class="exit-badge exit-definitive">Sortie definitive</div>
|
|
{% else %}
|
|
<div class="exit-badge exit-libre">Sortie libre</div>
|
|
{% endif %}
|
|
{% if design %}
|
|
<div class="invitation-badge" style="background: {{ inv_color }};">{{ inv_title }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="qr-section">
|
|
<div class="qr-box">
|
|
<img src="{{ qrBase64 }}" alt="QR Code">
|
|
</div>
|
|
<div class="ref-block">
|
|
<div class="ref-label">Reference</div>
|
|
<div class="ref-value">{{ ticket.reference }}</div>
|
|
<div class="ref-label" style="margin-top: 6px;">Commande</div>
|
|
<div class="ref-value">{{ order.reference }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="zone-hd">
|
|
{% if posterBase64 %}
|
|
<img src="{{ posterBase64 }}" alt="{{ event.title }}">
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="zone-bottom">
|
|
{% if logoBase64 %}
|
|
<img src="{{ logoBase64 }}" alt="Logo" class="org-logo">
|
|
{% endif %}
|
|
<div>
|
|
<div class="org-name">{{ organizer.companyName ?? (organizer.firstName ~ ' ' ~ organizer.lastName) }}</div>
|
|
{% if organizer.address %}
|
|
<div class="org-details">{{ organizer.address }}{% if organizer.postalCode %}, {{ organizer.postalCode }}{% endif %}{% if organizer.city %} {{ organizer.city }}{% endif %}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="powered">E-Ticket<br>by E-Cosplay</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|