5dfb63dacb
DomPDF renders embedded PNG images more reliably than inline SVG. Changed QR code generation to OUTPUT_IMAGE_PNG with base64 encoding, then embed as img tag in the PDF HTML.
303 lines
9.0 KiB
PHP
303 lines
9.0 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Order;
|
||
use Barryvdh\DomPDF\Facade\Pdf;
|
||
use chillerlan\QRCode\QRCode;
|
||
use chillerlan\QRCode\QROptions;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Illuminate\Support\Facades\Storage;
|
||
|
||
class QrStickerService
|
||
{
|
||
/**
|
||
* Generate both SVG and A6 PDF sticker for an order
|
||
*/
|
||
public function generateSticker(Order $order): array
|
||
{
|
||
Log::info('Starting QR sticker generation', [
|
||
'order_uuid' => $order->uuid,
|
||
'order_number' => $order->order_number,
|
||
]);
|
||
try {
|
||
// Generate QR code URL
|
||
$qrUrl = route('ops.order.show', ['token' => $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);
|
||
Log::info('SVG QR code generated', [
|
||
'order_uuid' => $order->uuid,
|
||
'svg_path' => $svgPath,
|
||
]);
|
||
// 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
|
||
{
|
||
$options = new QROptions([
|
||
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
|
||
'eccLevel' => QRCode::ECC_H,
|
||
'scale' => 3,
|
||
'imageBase64' => false,
|
||
]);
|
||
|
||
$qrCode = new QRCode($options);
|
||
$qrSvg = $qrCode->render($qrUrl);
|
||
|
||
$path = "qr-codes/{$order->uuid}.svg";
|
||
Storage::disk('public')->put($path, $qrSvg);
|
||
|
||
return $path;
|
||
}
|
||
|
||
/**
|
||
* Generate A6 PDF sticker (105mm × 148mm) with QR code and readable info
|
||
*/
|
||
private function generatePdf(Order $order, string $qrUrl): string
|
||
{
|
||
// Generate QR code as base64-encoded PNG (DomPDF handles this better than SVG)
|
||
$options = new QROptions([
|
||
'outputType' => QRCode::OUTPUT_IMAGE_PNG,
|
||
'eccLevel' => QRCode::ECC_H,
|
||
'scale' => 8,
|
||
'imageBase64' => true,
|
||
]);
|
||
|
||
$qrCode = new QRCode($options);
|
||
$qrBase64 = $qrCode->render($qrUrl);
|
||
|
||
// Determine order type
|
||
$orderType = $order->type === 'standard' ? 'STOCK ORDER' : 'CUSTOM ORDER';
|
||
|
||
// Extract design name
|
||
$designName = '';
|
||
if ($order->items && $order->items->count() > 0) {
|
||
$firstItem = $order->items->first();
|
||
$designName = $firstItem->product?->name ?? $firstItem->name ?? 'Custom Design';
|
||
}
|
||
|
||
// Extract customer surname
|
||
$customerName = $order->user?->name ?? '';
|
||
$nameParts = explode(' ', trim($customerName));
|
||
$customerSurname = end($nameParts);
|
||
|
||
// Build HTML with proper SVG embedding (avoid heredoc issues)
|
||
$html = '<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
|
||
.sticker {
|
||
width: 148mm;
|
||
height: 105mm;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 5mm;
|
||
background: white;
|
||
page-break-after: avoid;
|
||
}
|
||
.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: 50mm;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.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 {
|
||
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;
|
||
}
|
||
.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 {
|
||
width: 50mm;
|
||
height: 50mm;
|
||
}
|
||
.qr-code img {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
.footer {
|
||
font-size: 6pt;
|
||
color: #999;
|
||
text-align: center;
|
||
border-top: 0.5pt solid #eee;
|
||
padding-top: 1mm;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="sticker">
|
||
<div class="container">
|
||
<div class="info-section">
|
||
<div class="header">
|
||
<span class="order-type">' . htmlspecialchars($orderType) . '</span>
|
||
<div class="order-number">' . htmlspecialchars($order->order_number) . '</div>
|
||
</div>
|
||
<div class="customer-info">
|
||
<div class="label">Customer</div>
|
||
<div class="name">' . htmlspecialchars($customerSurname) . '</div>
|
||
</div>
|
||
<div class="details">
|
||
<div class="detail-row">
|
||
<span class="detail-label">Design:</span>
|
||
<span class="detail-value" style="font-size: 6.5pt;">' . htmlspecialchars($this->truncate($designName, 12)) . '</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Date:</span>
|
||
<span class="detail-value">' . htmlspecialchars($order->created_at->format('M d')) . '</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Status:</span>
|
||
<span class="detail-value">' . htmlspecialchars($this->formatStatus($order->status)) . '</span>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
Gen: ' . htmlspecialchars($order->created_at->format('Y-m-d H:i')) . '
|
||
</div>
|
||
</div>
|
||
<div class="qr-section">
|
||
<div class="qr-code">
|
||
<img src="' . $qrBase64 . '" alt="QR Code" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>';
|
||
|
||
try {
|
||
$pdf = Pdf::loadHTML($html);
|
||
// Set to A6 size (105mm × 148mm) in portrait orientation
|
||
// 105mm = 297.64 points, 148mm = 420.94 points
|
||
$pdf->setPaper([0, 0, 297.64, 420.94], 'portrait');
|
||
$pdf->setOption('isRemoteEnabled', true);
|
||
|
||
$filename = "{$order->uuid}.pdf";
|
||
$path = "qr-stickers/{$filename}";
|
||
$pdfContent = $pdf->output();
|
||
Storage::disk('public')->put($path, $pdfContent);
|
||
|
||
return $path;
|
||
} catch (\Exception $e) {
|
||
Log::error('DomPDF generation failed', [
|
||
'order_uuid' => $order->uuid,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper: Truncate string
|
||
*/
|
||
private function truncate($string, $length): string
|
||
{
|
||
return substr($string, 0, $length);
|
||
}
|
||
|
||
/**
|
||
* Helper: Format status text
|
||
*/
|
||
private function formatStatus($status): string
|
||
{
|
||
return str_replace('_', ' ', $status);
|
||
}
|
||
}
|