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_height' => $validated['height'], '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, (float) $validated['width'], (float) $validated['length'], (float) $validated['height'], (float) $validated['weight'], auth()->id() ); 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 $order->update(['status' => 'packing']); Log::info('Inspection passed via QR', [ 'order_uuid' => $order->uuid, 'marked_by' => auth()->user()->name, ]); // Emit event to trigger Slack/Trello updates \App\Events\InspectionPassed::dispatch($order); 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 \App\Events\InspectionFailed::dispatch($order, $validated['issue_description']); 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 { return match ($order->status) { 'inspection' => [ 'markInspectionPassed' => true, 'flagInspectionIssue' => true, ], 'packing' => [ 'confirmPacking' => true, ], 'ready_to_ship', 'awaiting_collection', 'in_transit' => [ 'readOnly' => true, ], default => [] }; } /** * 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" ); } /** * Re-download shipment PDFs from Shiplogic API * * POST /ops/orders/{id}/redownload-pdfs */ public function redownloadShipmentPdfs(Order $order) { // Authorize if (! auth()->user()->can('access-ops')) { abort(403, 'Unauthorized to access ops interface'); } // Only allow for orders with shipments if (! $order->courier_shipment_id) { return response()->json([ 'success' => false, 'message' => 'No shipment exists for this order', ], 422); } try { $courierService = new \App\Services\CourierService(); $result = $courierService->redownloadShipmentPdfs($order); Log::info('Shipment PDFs re-downloaded via ops', [ 'order_uuid' => $order->uuid, 'order_number' => $order->order_number, 'user_id' => auth()->id(), 'user_name' => auth()->user()->name, 'success' => $result['success'], ]); if ($result['success']) { return response()->json([ 'success' => true, 'message' => $result['message'], 'sticker_path' => $result['sticker_path'], 'waybill_path' => $result['waybill_path'], ]); } else { return response()->json([ 'success' => false, 'message' => $result['message'], ], 500); } } catch (\Exception $e) { Log::error('Failed to re-download shipment PDFs via ops', [ 'order_uuid' => $order->uuid, 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => 'Failed to re-download PDFs: ' . $e->getMessage(), ], 500); } } /** * Mark order as ready for collection and move Trello card * * POST /ops/orders/{id}/ready-for-collection */ public function markReadyForCollection(Order $order) { // Authorize if (! auth()->user()->can('access-ops')) { abort(403, 'Unauthorized to access ops interface'); } // Only allow for orders with shipments in ready_to_ship status if (! $order->courier_shipment_id || $order->status !== 'ready_to_ship') { return response()->json([ 'success' => false, 'message' => 'Order must have a shipment and be in Ready to Ship status', ], 422); } try { // Update order status $order->update([ 'status' => 'awaiting_collection', 'courier_status' => 'awaiting_collection', ]); // Move Trello card to Awaiting Collection if ($order->trello_card_id) { $trelloService = new \App\Services\TrelloService(); $trelloService->moveCard($order->trello_card_id, 'Awaiting Collection'); } Log::info('Order marked ready for collection via ops', [ 'order_uuid' => $order->uuid, 'order_number' => $order->order_number, 'user_id' => auth()->id(), 'user_name' => auth()->user()->name, ]); return response()->json([ 'success' => true, 'message' => 'Order moved to Awaiting Collection', ]); } catch (\Exception $e) { Log::error('Failed to mark order ready for collection', [ 'order_uuid' => $order->uuid, 'error' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'message' => 'Failed to mark ready for collection: ' . $e->getMessage(), ], 500); } }