93148ef9af
Generate QR as SVG and encode as base64 data URI for embedding in PDF. This avoids dependency on GD extension while keeping QR codes in PDF.
306 lines
9.1 KiB
PHP
306 lines
9.1 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 SVG, then convert to data URI
|
||
$options = new QROptions([
|
||
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
|
||
'eccLevel' => QRCode::ECC_H,
|
||
'scale' => 4,
|
||
'imageBase64' => false,
|
||
]);
|
||
|
||
$qrCode = new QRCode($options);
|
||
$qrSvg = $qrCode->render($qrUrl);
|
||
|
||
// Convert SVG to data URI for embedding in HTML
|
||
$qrDataUri = 'data:image/svg+xml;base64,' . base64_encode($qrSvg);
|
||
|
||
// 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="' . $qrDataUri . '" 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);
|
||
}
|
||
}
|