3040681842
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
239 lines
7.3 KiB
PHP
239 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Order;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class OpsController extends Controller
|
|
{
|
|
/**
|
|
* Display order details via QR token (state-machine driven UI)
|
|
*
|
|
* GET /ops/orders/{token}
|
|
*/
|
|
public function showOrder(Request $request, $token)
|
|
{
|
|
// Find order by QR token
|
|
$order = Order::where('qr_token', $token)->first();
|
|
|
|
if (! $order) {
|
|
Log::warning('QR token not found', ['token' => substr($token, 0, 8) . '...']);
|
|
|
|
return response()->view('ops.order-not-found', [], 404);
|
|
}
|
|
|
|
// Check user authorization - must be logged in ops user
|
|
// TODO: Add middleware to enforce this
|
|
if (! auth()->check() || ! auth()->user()->can('access-ops')) {
|
|
Log::warning('Unauthorized QR access attempt', [
|
|
'order_uuid' => $order->uuid,
|
|
'user_id' => auth()->id(),
|
|
]);
|
|
|
|
return response()->view('ops.unauthorized', [], 403);
|
|
}
|
|
|
|
Log::info('QR order accessed', [
|
|
'order_uuid' => $order->uuid,
|
|
'status' => $order->status,
|
|
'user_id' => auth()->id(),
|
|
]);
|
|
|
|
// Determine available actions based on order status
|
|
$availableActions = $this->getAvailableActions($order);
|
|
|
|
return view('ops.order-detail', [
|
|
'order' => $order,
|
|
'availableActions' => $availableActions,
|
|
'qrUrl' => route('ops.order.show', ['token' => $order->qr_token]),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Confirm packing via QR form
|
|
*
|
|
* POST /ops/orders/{id}/pack
|
|
*/
|
|
public function confirmPacking(Request $request, Order $order)
|
|
{
|
|
// Validate input
|
|
$validated = $request->validate([
|
|
'weight' => ['required', 'numeric', 'min:0.1'],
|
|
'width' => ['required', 'numeric', 'min:1'],
|
|
'length' => ['required', 'numeric', 'min:1'],
|
|
'height' => ['required', 'numeric', 'min:1'],
|
|
]);
|
|
|
|
// Ensure order is in packing state
|
|
if ($order->status !== 'packing' && $order->status !== 'inspection') {
|
|
return response()->json([
|
|
'error' => 'Order cannot be packed in current state',
|
|
'current_status' => $order->status,
|
|
], 409);
|
|
}
|
|
|
|
// Save packing dimensions
|
|
$order->update([
|
|
'packing_width' => $validated['width'],
|
|
'packing_length' => $validated['length'],
|
|
'packing_weight' => $validated['weight'],
|
|
'packing_completed_at' => now(),
|
|
'packed_by' => auth()->id(),
|
|
'status' => 'packing',
|
|
]);
|
|
|
|
Log::info('Order packed via QR', [
|
|
'order_uuid' => $order->uuid,
|
|
'packed_by' => auth()->user()->name,
|
|
'dimensions' => "{$validated['width']}x{$validated['length']}cm, {$validated['weight']}kg",
|
|
]);
|
|
|
|
// Emit event to trigger Slack notification and Trello update
|
|
\App\Events\OrderPacked::dispatch($order);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Order packed successfully',
|
|
'order' => [
|
|
'uuid' => $order->uuid,
|
|
'number' => $order->order_number,
|
|
'status' => $order->status,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Mark inspection passed via QR
|
|
*
|
|
* POST /ops/orders/{id}/inspection-passed
|
|
*/
|
|
public function markInspectionPassed(Request $request, Order $order)
|
|
{
|
|
if ($order->status !== 'inspection') {
|
|
return response()->json([
|
|
'error' => 'Order is not in inspection state',
|
|
'current_status' => $order->status,
|
|
], 409);
|
|
}
|
|
|
|
// Update status and emit event
|
|
$order->update(['status' => 'packing']);
|
|
|
|
Log::info('Inspection passed via QR', [
|
|
'order_uuid' => $order->uuid,
|
|
'marked_by' => auth()->user()->name,
|
|
]);
|
|
|
|
// Emit event for listeners to handle Slack/Trello updates
|
|
// TODO: Create InspectionPassed event
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Inspection passed',
|
|
'order' => ['uuid' => $order->uuid, 'status' => $order->status],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Flag inspection issue via QR
|
|
*
|
|
* POST /ops/orders/{id}/inspection-failed
|
|
*/
|
|
public function flagInspectionIssue(Request $request, Order $order)
|
|
{
|
|
$validated = $request->validate([
|
|
'issue_description' => ['required', 'string', 'max:500'],
|
|
]);
|
|
|
|
if ($order->status !== 'inspection') {
|
|
return response()->json([
|
|
'error' => 'Order is not in inspection state',
|
|
'current_status' => $order->status,
|
|
], 409);
|
|
}
|
|
|
|
// Update status and store issue
|
|
$order->update([
|
|
'status' => 'review_required',
|
|
'notes' => $validated['issue_description'],
|
|
]);
|
|
|
|
Log::warning('Inspection issue flagged via QR', [
|
|
'order_uuid' => $order->uuid,
|
|
'flagged_by' => auth()->user()->name,
|
|
'issue' => $validated['issue_description'],
|
|
]);
|
|
|
|
// Emit event for listeners to alert ops
|
|
// TODO: Create InspectionFailed event
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Issue flagged - order moved to review',
|
|
'order' => ['uuid' => $order->uuid, 'status' => $order->status],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Determine which actions are available based on order status
|
|
*/
|
|
private function getAvailableActions(Order $order): array
|
|
{
|
|
$actions = [];
|
|
|
|
match ($order->status) {
|
|
'inspection' => [
|
|
$actions['markInspectionPassed'] = true,
|
|
$actions['flagInspectionIssue'] = true,
|
|
],
|
|
'packing' => [
|
|
$actions['confirmPacking'] = true,
|
|
],
|
|
'ready_to_ship', 'awaiting_collection', 'in_transit' => [
|
|
$actions['readOnly'] = true,
|
|
],
|
|
default => []
|
|
};
|
|
|
|
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"
|
|
);
|
|
}
|
|
}
|