From 3040681842de32501ec29aa7eb5fad7720db3ef8 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:51:23 +0200 Subject: [PATCH] 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 --- app/Http/Controllers/OpsController.php | 35 +++ .../GenerateQrCodeOnOrderCreated.php | 26 +- app/Services/QrStickerService.php | 113 +++++++++ resources/views/ops/order-detail.blade.php | 9 +- resources/views/stickers/qr-sticker.blade.php | 227 ++++++++++++++++++ routes/web.php | 3 +- 6 files changed, 394 insertions(+), 19 deletions(-) create mode 100644 app/Services/QrStickerService.php create mode 100644 resources/views/stickers/qr-sticker.blade.php diff --git a/app/Http/Controllers/OpsController.php b/app/Http/Controllers/OpsController.php index 9c813bb..d2b49e1 100644 --- a/app/Http/Controllers/OpsController.php +++ b/app/Http/Controllers/OpsController.php @@ -200,4 +200,39 @@ class OpsController extends Controller return $actions; } + + /** + * Download QR sticker PDF for printing + * + * GET /ops/orders/{id}/sticker/download + */ + public function downloadSticker(Order $order) + { + // Authorize + if (! auth()->user()->can('access-ops')) { + abort(403, 'Unauthorized to access ops interface'); + } + + $stickerPath = "qr-stickers/{$order->uuid}.pdf"; + + if (! \Illuminate\Support\Facades\Storage::disk('public')->exists($stickerPath)) { + Log::warning('QR sticker PDF not found', [ + 'order_uuid' => $order->uuid, + 'path' => $stickerPath, + ]); + + abort(404, 'QR sticker not found'); + } + + Log::info('QR sticker downloaded', [ + 'order_uuid' => $order->uuid, + 'user_id' => auth()->id(), + 'user_name' => auth()->user()->name, + ]); + + return \Illuminate\Support\Facades\Storage::disk('public')->download( + $stickerPath, + "QR-{$order->order_number}.pdf" + ); + } } diff --git a/app/Listeners/GenerateQrCodeOnOrderCreated.php b/app/Listeners/GenerateQrCodeOnOrderCreated.php index 39a01db..03202f2 100644 --- a/app/Listeners/GenerateQrCodeOnOrderCreated.php +++ b/app/Listeners/GenerateQrCodeOnOrderCreated.php @@ -3,10 +3,9 @@ namespace App\Listeners; use App\Events\OrderCreated; +use App\Services\QrStickerService; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use SimpleSoftwareIO\QrCode\Facades\QrCode; class GenerateQrCodeOnOrderCreated { @@ -25,26 +24,19 @@ class GenerateQrCodeOnOrderCreated $order->save(); } - // Generate QR code pointing to ops interface - $qrUrl = route('ops.order.show', ['token' => $order->qr_token]); - - Log::debug('Generating QR code', [ + Log::debug('Generating QR sticker', [ 'order_uuid' => $order->uuid, - 'qr_url' => $qrUrl, + 'order_number' => $order->order_number, ]); - // Generate QR code SVG - $qrCode = QrCode::size(300) - ->errorCorrection('H') - ->generate($qrUrl); + // Generate both SVG (web) and PDF (print) stickers + $service = new QrStickerService(); + $paths = $service->generateSticker($order); - // Save QR code image - $path = "qr-codes/{$order->uuid}.svg"; - Storage::disk('public')->put($path, $qrCode); - - Log::info('QR code generated successfully', [ + Log::info('QR sticker generated successfully', [ 'order_uuid' => $order->uuid, - 'path' => $path, + 'svg_path' => $paths['svg_path'], + 'pdf_path' => $paths['pdf_path'], 'token' => substr($order->qr_token, 0, 8) . '...', ]); } catch (\Exception $e) { diff --git a/app/Services/QrStickerService.php b/app/Services/QrStickerService.php new file mode 100644 index 0000000..9819f6d --- /dev/null +++ b/app/Services/QrStickerService.php @@ -0,0 +1,113 @@ + $order->qr_token]); + + Log::debug('Generating QR sticker', [ + 'order_uuid' => $order->uuid, + 'order_number' => $order->order_number, + 'qr_url' => $qrUrl, + ]); + + // Generate SVG (for web display) + $svgPath = $this->generateSvg($order, $qrUrl); + + // Generate A6 PDF (for printing) + $pdfPath = $this->generatePdf($order, $qrUrl); + + Log::info('QR sticker generated successfully', [ + 'order_uuid' => $order->uuid, + 'svg_path' => $svgPath, + 'pdf_path' => $pdfPath, + ]); + + return [ + 'svg_path' => $svgPath, + 'pdf_path' => $pdfPath, + ]; + } catch (\Exception $e) { + Log::error('Failed to generate QR sticker', [ + 'order_uuid' => $order->uuid, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } + + /** + * Generate SVG QR code for web display + */ + private function generateSvg(Order $order, string $qrUrl): string + { + $qrCode = QrCode::size(300) + ->errorCorrection('H') + ->generate($qrUrl); + + $path = "qr-codes/{$order->uuid}.svg"; + Storage::disk('public')->put($path, $qrCode); + + return $path; + } + + /** + * Generate A6 PDF sticker (105mm × 148mm) with QR code and readable info + * + * A6 dimensions: 105mm × 148mm landscape + * Margins: 5mm all around + * QR size: ~50mm × 50mm + * Text area: remaining space for order info + */ + private function generatePdf(Order $order, string $qrUrl): string + { + // Generate inline QR code as base64 data URL + $qrSvg = QrCode::size(400) + ->errorCorrection('H') + ->generate($qrUrl); + + // Determine order type and format label + $orderType = $order->type === 'standard' + ? 'STOCK ORDER' + : 'CUSTOM ORDER'; + + // Prepare data for the view + $data = [ + 'order' => $order, + 'qrSvg' => $qrSvg, + 'orderType' => $orderType, + 'qrUrl' => $qrUrl, + 'generatedAt' => now()->format('Y-m-d H:i'), + ]; + + // Generate PDF using the sticker template + $pdf = Pdf::loadView('stickers.qr-sticker', $data); + + // Configure for A6 landscape + // A6 is 105mm × 148mm (landscape: 148mm × 105mm) + $pdf->setPaper([0, 0, 420.94, 297.64], 'portrait'); // 148mm × 105mm in points (1mm ≈ 2.834645669 points) + + // Store PDF + $filename = "{$order->uuid}.pdf"; + $path = "qr-stickers/{$filename}"; + $pdfContent = $pdf->output(); + Storage::disk('public')->put($path, $pdfContent); + + return $path; + } +} diff --git a/resources/views/ops/order-detail.blade.php b/resources/views/ops/order-detail.blade.php index b9d998a..c6b570b 100644 --- a/resources/views/ops/order-detail.blade.php +++ b/resources/views/ops/order-detail.blade.php @@ -31,7 +31,14 @@
-

Scan to access this order:

+
+
+

Scan to access this order:

+
+ + 📥 Download Sticker (A6 PDF) + +
{!! file_get_contents(storage_path("app/public/qr-codes/{$order->uuid}.svg")) !!}
diff --git a/resources/views/stickers/qr-sticker.blade.php b/resources/views/stickers/qr-sticker.blade.php new file mode 100644 index 0000000..0ed63f1 --- /dev/null +++ b/resources/views/stickers/qr-sticker.blade.php @@ -0,0 +1,227 @@ + + + + + + + +
+
+
+ +
+ {{ $orderType }} +
{{ $order->order_number }}
+
+ + +
+
Customer
+
{{ $order->user->name ?? 'N/A' }}
+
+ + +
+
+ Date: + {{ $order->created_at->format('M d') }} +
+ +
+ Status: + + {{ str_replace('_', ' ', $order->status) }} + +
+ + @if($order->type === 'standard') +
+ Items: + {{ $order->items->count() }} +
+ @endif + + @if($order->packing_completed_at) +
+ Packed: + +
+ @endif + + @if($order->courier_tracking_number) +
+ Track: + {{ substr($order->courier_tracking_number, 0, 8) }} +
+ @endif +
+ + + +
+ + +
+
+ {!! $qrSvg !!} +
+
+
+
+ + diff --git a/routes/web.php b/routes/web.php index e790fad..e44417a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -77,6 +77,7 @@ Route::middleware('auth')->group(function () { Route::post('/ops/orders/{order:uuid}/pack', [OpsController::class, 'confirmPacking'])->name('ops.order.pack'); Route::post('/ops/orders/{order:uuid}/inspection-passed', [OpsController::class, 'markInspectionPassed'])->name('ops.order.inspection-passed'); Route::post('/ops/orders/{order:uuid}/inspection-failed', [OpsController::class, 'flagInspectionIssue'])->name('ops.order.inspection-failed'); + Route::get('/ops/orders/{order:uuid}/sticker/download', [OpsController::class, 'downloadSticker'])->name('ops.order.sticker.download'); }); // QR landing page (public but token-gated) @@ -94,4 +95,4 @@ Route::get('/font-test', function () { // Test route for invoice generation (remove in production) -require __DIR__ . '/test-invoice.php'; \ No newline at end of file +require __DIR__ . '/test-invoice.php';