feat: Add A6 PDF QR stickers with human-readable order info

Implement QrStickerService to generate both SVG (web) and PDF (print) stickers:

- Generate 32x32mm QR codes using SimpleSoftwareIO
- Create A6 (148mm  105mm) PDF stickers using DomPDF
- Include human-readable info: order number, customer, status, date, tracking
- Store order type (STOCK/CUSTOM) with visual badge
- Add generation timestamp to footer

Create stickers/qr-sticker.blade.php template:
- Two-column layout: info left, QR right
- Styled for A6 landscape printing
- Shows order number, customer, status, date, tracking info
- QR code positioned for easy scanning
- Print-optimized CSS

Update GenerateQrCodeOnOrderCreated listener:
- Now calls QrStickerService::generateSticker()
- Generates both SVG and PDF on OrderCreated event
- Logs paths to both formats

Add OpsController::downloadSticker() endpoint:
- GET /ops/orders/{id}/sticker/download
- Returns PDF with filename QR-{order_number}.pdf
- Requires ops access authorization
- Logs all downloads with user context

Update ops order-detail view:
- Add download button for A6 sticker PDF
- Position next to QR code display
- Link to new sticker download route
This commit is contained in:
twotalesanimation
2026-01-02 19:51:23 +02:00
parent bb6b92df10
commit 3040681842
6 changed files with 394 additions and 19 deletions
+8 -1
View File
@@ -31,7 +31,14 @@
<!-- QR Code -->
<div class="border-t pt-6">
<p class="text-sm text-gray-600 mb-3">Scan to access this order:</p>
<div class="flex items-start justify-between mb-3">
<div>
<p class="text-sm text-gray-600 mb-3">Scan to access this order:</p>
</div>
<a href="{{ route('ops.order.sticker.download', $order) }}" class="inline-flex items-center px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm">
📥 Download Sticker (A6 PDF)
</a>
</div>
<div class="inline-block bg-gray-50 p-4 rounded border">
{!! file_get_contents(storage_path("app/public/qr-codes/{$order->uuid}.svg")) !!}
</div>
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Arial, sans-serif;
}
/* A6 Landscape: 148mm × 105mm */
.sticker {
width: 148mm;
height: 105mm;
display: flex;
flex-direction: column;
padding: 5mm;
background: white;
position: relative;
}
/* Split layout: QR on right (50mm), info on left */
.container {
display: flex;
gap: 3mm;
height: 100%;
}
.info-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.qr-section {
width: 55mm;
display: flex;
align-items: center;
justify-content: center;
}
/* Header with order type badge */
.header {
border-bottom: 1pt solid #ddd;
padding-bottom: 2mm;
}
.order-type {
display: inline-block;
background: #333;
color: white;
padding: 1mm 2mm;
font-size: 7pt;
font-weight: bold;
letter-spacing: 0.5pt;
}
.order-number {
font-size: 9pt;
font-weight: bold;
margin-top: 1mm;
color: #222;
word-break: break-all;
}
/* Customer info */
.customer-info {
font-size: 8pt;
color: #555;
line-height: 1.3;
}
.customer-info .label {
font-size: 7pt;
color: #999;
text-transform: uppercase;
letter-spacing: 0.3pt;
}
.customer-info .name {
font-weight: 500;
color: #222;
}
/* Order details */
.details {
font-size: 7.5pt;
color: #666;
line-height: 1.4;
}
.detail-row {
display: flex;
justify-content: space-between;
gap: 2mm;
margin-bottom: 1mm;
}
.detail-label {
font-weight: 500;
color: #999;
text-transform: uppercase;
font-size: 6.5pt;
flex: 1;
}
.detail-value {
text-align: right;
color: #222;
flex: 0 0 auto;
}
/* QR code */
.qr-code {
width: 50mm;
height: 50mm;
display: flex;
align-items: center;
justify-content: center;
background: white;
}
.qr-code svg {
width: 100%;
height: 100%;
}
/* Footer timestamp */
.footer {
font-size: 6pt;
color: #999;
text-align: center;
border-top: 0.5pt solid #eee;
padding-top: 1mm;
margin-top: auto;
}
/* Print settings */
@media print {
body {
margin: 0;
padding: 0;
}
.sticker {
page-break-after: avoid;
margin: 0;
box-shadow: none;
}
}
</style>
</head>
<body>
<div class="sticker">
<div class="container">
<div class="info-section">
<!-- Header -->
<div class="header">
<span class="order-type">{{ $orderType }}</span>
<div class="order-number">{{ $order->order_number }}</div>
</div>
<!-- Customer Info -->
<div class="customer-info">
<div class="label">Customer</div>
<div class="name">{{ $order->user->name ?? 'N/A' }}</div>
</div>
<!-- Order Details -->
<div class="details">
<div class="detail-row">
<span class="detail-label">Date:</span>
<span class="detail-value">{{ $order->created_at->format('M d') }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status:</span>
<span class="detail-value" style="text-transform: capitalize;">
{{ str_replace('_', ' ', $order->status) }}
</span>
</div>
@if($order->type === 'standard')
<div class="detail-row">
<span class="detail-label">Items:</span>
<span class="detail-value">{{ $order->items->count() }}</span>
</div>
@endif
@if($order->packing_completed_at)
<div class="detail-row">
<span class="detail-label">Packed:</span>
<span class="detail-value"></span>
</div>
@endif
@if($order->courier_tracking_number)
<div class="detail-row">
<span class="detail-label">Track:</span>
<span class="detail-value" style="font-size: 6.5pt;">{{ substr($order->courier_tracking_number, 0, 8) }}</span>
</div>
@endif
</div>
<!-- Footer -->
<div class="footer">
Gen: {{ $generatedAt }}
</div>
</div>
<!-- QR Code Section -->
<div class="qr-section">
<div class="qr-code">
{!! $qrSvg !!}
</div>
</div>
</div>
</div>
</body>
</html>