From 74525e8955aecf18cbfc1909de765954cd4a9025 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:55:53 +0200 Subject: [PATCH] feat: Implement QR code generation and ops interface scaffold Phase 1: QR Code Generation - Create GenerateQrCodeOnOrderCreated listener - Generate secure random token on order creation - Create SVG QR codes pointing to /ops/orders/{token} - Store QR token and generation timestamp on order record - Register listener in EventServiceProvider Phase 2: Ops Controller & Routes - Create OpsController with showOrder() landing page - Implement confirmPacking() to capture dimensions via form - Add markInspectionPassed() and flagInspectionIssue() methods - Implement getAvailableActions() state machine for UI - Add routes: GET /ops/orders/{token}, POST /ops/orders/{id}/pack, etc. - Token-gated access, requires auth middleware Next: Create Blade templates for state-driven UI --- app/Http/Controllers/OpsController.php | 203 ++++++++++++++++++ .../GenerateQrCodeOnOrderCreated.php | 57 +++++ app/Providers/EventServiceProvider.php | 2 + routes/web.php | 11 +- 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/OpsController.php create mode 100644 app/Listeners/GenerateQrCodeOnOrderCreated.php diff --git a/app/Http/Controllers/OpsController.php b/app/Http/Controllers/OpsController.php new file mode 100644 index 0000000..9c813bb --- /dev/null +++ b/app/Http/Controllers/OpsController.php @@ -0,0 +1,203 @@ +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; + } +} diff --git a/app/Listeners/GenerateQrCodeOnOrderCreated.php b/app/Listeners/GenerateQrCodeOnOrderCreated.php new file mode 100644 index 0000000..39a01db --- /dev/null +++ b/app/Listeners/GenerateQrCodeOnOrderCreated.php @@ -0,0 +1,57 @@ +order; + + try { + // Generate secure token if not already set + if (! $order->qr_token) { + $order->qr_token = Str::random(32); + $order->qr_generated_at = now(); + $order->save(); + } + + // Generate QR code pointing to ops interface + $qrUrl = route('ops.order.show', ['token' => $order->qr_token]); + + Log::debug('Generating QR code', [ + 'order_uuid' => $order->uuid, + 'qr_url' => $qrUrl, + ]); + + // Generate QR code SVG + $qrCode = QrCode::size(300) + ->errorCorrection('H') + ->generate($qrUrl); + + // Save QR code image + $path = "qr-codes/{$order->uuid}.svg"; + Storage::disk('public')->put($path, $qrCode); + + Log::info('QR code generated successfully', [ + 'order_uuid' => $order->uuid, + 'path' => $path, + 'token' => substr($order->qr_token, 0, 8) . '...', + ]); + } catch (\Exception $e) { + Log::error('Failed to generate QR code', [ + 'order_uuid' => $order->uuid, + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 027e148..d8eab82 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -17,6 +17,7 @@ use App\Events\ReadyToShipIntent; use App\Events\ShipmentCreated; use App\Events\ShipmentCreationFailed; use App\Listeners\CreateShipmentOnReadyToShip; +use App\Listeners\GenerateQrCodeOnOrderCreated; use App\Listeners\NotifySlackOnOrderCreated; use App\Listeners\NotifySlackOnOrderPacked; use App\Listeners\NotifySlackOnParcelCollected; @@ -39,6 +40,7 @@ class EventServiceProvider extends ServiceProvider // Order events OrderCreated::class => [ NotifySlackOnOrderCreated::class, + GenerateQrCodeOnOrderCreated::class, ], // Packing events diff --git a/routes/web.php b/routes/web.php index adc1f62..e790fad 100644 --- a/routes/web.php +++ b/routes/web.php @@ -10,6 +10,7 @@ use App\Http\Controllers\CartController; use App\Http\Controllers\OrderController; use App\Http\Controllers\CustomOrderController; use App\Http\Controllers\Auth\GoogleAuthController; +use App\Http\Controllers\OpsController; use App\Http\Controllers\PackingController; use App\Http\Controllers\ShippingController; @@ -71,7 +72,15 @@ Route::middleware('auth')->group(function () { Route::post('/orders/{order:uuid}/pack', [PackingController::class, 'confirmPacked'])->name('orders.pack'); Route::post('/custom-orders/{customOrder:uuid}/pack', [PackingController::class, 'confirmPacked'])->name('custom-orders.pack'); Route::post('/orders/{order:uuid}/ship', [ShippingController::class, 'createShipment'])->name('orders.ship'); - Route::post('/custom-orders/{customOrder:uuid}/ship', [ShippingController::class, 'createShipment'])->name('custom-orders.ship'); + + // QR-based operations (ops interface) + 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'); +}); + +// QR landing page (public but token-gated) +Route::get('/ops/orders/{token}', [OpsController::class, 'showOrder'])->name('ops.order.show' Route::post('/custom-orders/{customOrder:uuid}/ship', [ShippingController::class, 'createShipment'])->name('custom-orders.ship'); }); // use Illuminate\Support\Facades\Route;