Fix PDF template for dompdf (table layout instead of CSS Grid), add order number everywhere

- Rewrite pdf/billet.html.twig with table-based layout compatible with dompdf
- Add order number (orderNumber) and order reference on PDF ticket
- Show order number on all order pages: guest, payment, public, summary partial

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-21 16:30:04 +01:00
parent 9af0d5c1a5
commit 50c8fe17a7
4 changed files with 305 additions and 140 deletions

View File

@@ -20,6 +20,9 @@
<span class="font-black text-lg text-indigo-600">{{ order.totalHTDecimal|number_format(2, ',', ' ') }} &euro;</span>
</div>
<p class="text-[10px] font-bold text-gray-400 mt-2">Ref: {{ order.reference }}</p>
{% if order.orderNumber %}
<p class="text-[10px] font-bold text-gray-400 mt-2">Commande {{ order.orderNumber }}</p>
{% endif %}
<p class="text-[10px] font-bold text-gray-400">Ref: {{ order.reference }}</p>
</div>
</div>

View File

@@ -5,7 +5,8 @@
{% block body %}
<div class="page-container">
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Vos informations</h1>
<p class="font-bold text-gray-600 italic mb-8">Commande {{ order.reference }}{{ order.event.title }}</p>
<p class="font-bold text-gray-600 italic mb-2">Commande {{ order.orderNumber }}{{ order.event.title }}</p>
<p class="text-xs font-mono text-gray-400 mb-8">Ref: {{ order.reference }}</p>
{% for message in app.flashes('error') %}
<div class="flash-error"><p class="font-black text-sm">{{ message }}</p></div>

View File

@@ -5,7 +5,8 @@
{% block body %}
<div class="page-container">
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Ma commande</h1>
<p class="font-bold text-gray-600 italic mb-8">{{ order.reference }}{{ order.event.title }}</p>
<p class="font-bold text-gray-600 italic mb-2">Commande {{ order.orderNumber }}{{ order.event.title }}</p>
<p class="text-xs font-mono text-gray-400 mb-8">Ref: {{ order.reference }}</p>
<div class="flex flex-col lg:flex-row gap-8">
<div class="flex-1">

View File

@@ -22,175 +22,335 @@
.ticket {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.zone-top {
flex: 1;
display: flex;
min-height: 0;
/* ====== TOP ROW: two columns via table ====== */
.top-row {
width: 100%;
height: 702px;
border-collapse: collapse;
}
.top-row td {
vertical-align: top;
}
/* ====== HG : infos evenement + billet ====== */
.zone-hg {
flex: 1;
width: 375px;
padding: 28px 20px 20px 32px;
border-right: 3px solid {{ ac }};
border-bottom: 3px solid {{ ac }};
display: flex;
flex-direction: column;
}
.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;
}
.zone-hd {
width: 220px;
flex-shrink: 0;
border-bottom: 3px solid {{ ac }};
display: flex;
align-items: center;
justify-content: center;
background: #fafafa;
overflow: hidden;
.info-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 4px;
}
.zone-hd img {
max-width: 100%;
max-height: 100%;
.info-table td {
padding: 4px 0;
border-bottom: 1px solid #11111110;
}
.info-table tr:last-child td {
border-bottom: none;
}
.info-label {
width: 65px;
font-size: 7px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #666;
}
.info-value {
font-size: 11px;
font-weight: 700;
}
.zone-bottom {
padding: 16px 32px;
display: flex;
align-items: center;
gap: 14px;
.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-table {
width: 100%;
border-collapse: collapse;
}
.meta-table td {
padding: 2px 8px 2px 0;
vertical-align: top;
}
.meta-label {
font-size: 7px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #666;
}
.meta-value {
font-size: 10px;
font-weight: 700;
margin-bottom: 2px;
}
.badges {
margin-top: 10px;
}
.exit-badge {
display: inline-block;
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 {
display: inline-block;
padding: 4px 8px;
font-size: 8px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 2px;
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; }
/* QR + ref */
.qr-section {
margin-top: 16px;
padding-top: 10px;
border-top: 1px solid #11111112;
}
.qr-table {
width: 100%;
border-collapse: collapse;
}
.qr-table td {
vertical-align: bottom;
}
.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;
color: #999;
}
.ref-value {
font-size: 9px;
font-weight: 700;
font-family: monospace;
color: #666;
}
.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; }
/* ====== HD : affiche ====== */
.zone-hd {
width: 220px;
border-bottom: 3px solid {{ ac }};
text-align: center;
background: #fafafa;
}
.zone-hd img {
max-width: 200px;
max-height: 680px;
}
.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; }
/* ====== BOTTOM : infos association ====== */
.zone-bottom {
width: 100%;
padding: 16px 32px;
background: {{ ac }};
color: #fff;
}
.zone-bottom table {
width: 100%;
border-collapse: collapse;
}
.zone-bottom td {
vertical-align: middle;
}
.org-logo img {
width: 45px;
height: 45px;
}
.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;
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>
<table class="top-row">
<tr>
<td 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>
<table class="info-table">
<tr>
<td class="info-label">Date</td>
<td class="info-value">{{ event.startAt|date('d/m/Y') }}</td>
</tr>
<tr>
<td class="info-label">Horaires</td>
<td class="info-value">{{ event.startAt|date('H:i') }}{{ event.endAt|date('H:i') }}</td>
</tr>
<tr>
<td class="info-label">Lieu</td>
<td class="info-value">{{ event.address }}</td>
</tr>
<tr>
<td class="info-label">Ville</td>
<td class="info-value">{{ event.zipcode }} {{ event.city }}</td>
</tr>
</table>
<div class="separator"></div>
<div class="separator"></div>
<div class="billet-name">{{ ticket.billetName }}</div>
<div class="billet-price">{{ ticket.unitPriceHTDecimal|number_format(2, ',', ' ') }} &euro; HT</div>
<div class="billet-name">{{ ticket.billetName }}</div>
<div class="billet-price">{{ ticket.unitPriceHTDecimal|number_format(2, ',', ' ') }} &euro; HT</div>
<div class="meta-grid">
<div>
<div class="meta-label">Categorie</div>
<div class="meta-value">{{ ticket.billet.category.name }}</div>
<table class="meta-table">
<tr>
<td>
<div class="meta-label">Categorie</div>
<div class="meta-value">{{ ticket.billet.category.name }}</div>
</td>
<td>
<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>
</td>
</tr>
<tr>
<td>
<div class="meta-label">Acheteur</div>
<div class="meta-value">{{ order.firstName }} {{ order.lastName }}</div>
</td>
<td>
<div class="meta-label">E-mail</div>
<div class="meta-value">{{ order.email }}</div>
</td>
</tr>
</table>
<div class="badges">
{% if ticket.billet.definedExit %}
<span class="exit-badge exit-definitive">Sortie definitive</span>
{% else %}
<span class="exit-badge exit-libre">Sortie libre</span>
{% endif %}
{% if design %}
<span class="invitation-badge" style="background: {{ inv_color }};">{{ inv_title }}</span>
{% endif %}
</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>
<div class="qr-section">
<table class="qr-table">
<tr>
<td class="qr-box">
<img src="{{ qrBase64 }}" alt="QR Code">
</td>
<td class="ref-block">
<div class="ref-label">Reference billet</div>
<div class="ref-value">{{ ticket.reference }}</div>
<div class="ref-label" style="margin-top: 6px;">Commande</div>
<div class="ref-value">{{ order.orderNumber }}</div>
<div class="ref-label" style="margin-top: 6px;">Ref commande</div>
<div class="ref-value">{{ order.reference }}</div>
</td>
</tr>
</table>
</div>
</td>
<td class="zone-hd">
{% if posterBase64 %}
<img src="{{ posterBase64 }}" alt="{{ event.title }}">
{% 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>
</td>
</tr>
</table>
<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>
<table>
<tr>
{% if logoBase64 %}
<td class="org-logo" style="width: 60px; padding-right: 14px;">
<img src="{{ logoBase64 }}" alt="Logo">
</td>
{% endif %}
<td>
<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 %}
</td>
<td class="powered">E-Ticket<br>by E-Cosplay</td>
</tr>
</table>
</div>
</div>
</body>