ed57956694
When a new order is created and a Trello card is made, now automatically generate the A6 QR sticker PDF and attach it to the card using Trello's attachment API. Include error handling and logging.
293 lines
8.8 KiB
PHP
293 lines
8.8 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: 2mm;
|
||
height: 100%;
|
||
}
|
||
.info-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
overflow: hidden;
|
||
}
|
||
.qr-section {
|
||
width: 100mm;
|
||
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: 16pt;
|
||
font-weight: bold;
|
||
margin-top: 1mm;
|
||
color: #222;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.details {
|
||
font-size: 10pt;
|
||
color: #666;
|
||
line-height: 1.4;
|
||
}
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 2mm;
|
||
margin-bottom: 1mm;
|
||
}
|
||
.detail-label {
|
||
|
||
color: #999;
|
||
text-transform: uppercase;
|
||
flex: 1;
|
||
}
|
||
.detail-value {
|
||
text-align: right;
|
||
font-weight: bold;
|
||
text-transform: uppercase;
|
||
color: #222;
|
||
flex: 0 0 auto;
|
||
}
|
||
.qr-code {
|
||
width: 95mm;
|
||
height: 95mm;
|
||
}
|
||
.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="details">
|
||
<div class="detail-row">
|
||
<span class="detail-label">Customer: </span>
|
||
<span class="detail-value">' . htmlspecialchars($customerSurname) . '</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Design:</span>
|
||
<span class="detail-value">' . 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>
|
||
<div class="qr-code">
|
||
<img src="' . $qrDataUri . '" alt="QR Code" />
|
||
</div>
|
||
|
||
<div class="footer">
|
||
Gen: ' . htmlspecialchars($order->created_at->format('Y-m-d H:i')) . '
|
||
</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);
|
||
}
|
||
}
|