middleware('auth'); $this->middleware('admin')->except(['show', 'track']); } /** * Test the DHL API login credentials and return a JSON response. * * @return \Illuminate\Http\JsonResponse */ public function testLogin() { try { // Get DHL configuration with admin settings $settingController = new \App\Http\Controllers\SettingController(); $dhlConfig = $settingController->getDhlConfig(); // Create DhlClient with merged configuration $dhlClient = new \Acme\Dhl\Support\DhlClient( $dhlConfig['base_url'], $dhlConfig['api_key'], $dhlConfig['username'], $dhlConfig['password'] ); // Test the connection $connectionTest = $dhlClient->testConnection(); if ($connectionTest) { $result = [ 'success' => true, 'message' => 'DHL API Verbindung erfolgreich getestet!', 'details' => [ 'base_url' => $dhlConfig['base_url'], 'using_admin_config' => !empty($dhlConfig['api_key']) ] ]; } else { $result = [ 'success' => false, 'message' => 'DHL API Verbindung fehlgeschlagen. Prüfen Sie Ihre Zugangsdaten.' ]; } return response()->json($result); } catch (Exception $e) { Log::error('[DHL Controller] Test login failed', [ 'error' => $e->getMessage() ]); return response()->json([ 'success' => false, 'message' => 'DHL API Test fehlgeschlagen: ' . $e->getMessage() ], 500); } } /** * Display the DHL Cockpit (main overview) * * @param Request $request * @return View */ public function index(Request $request): View { // Statistics for dashboard widgets $stats = [ 'total_shipments' => DhlShipment::count(), 'pending_shipments' => DhlShipment::where('status', 'pending')->count(), 'shipped_today' => DhlShipment::whereDate('created_at', today())->count(), 'returns_count' => DhlShipment::where('type', 'return')->count(), ]; return view('admin.dhl.cockpit', compact('stats')); } /** * Provides data for the DHL Cockpit DataTable. * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function datatable(Request $request): JsonResponse { $query = DhlShipment::with(['shoppingOrder.shopping_user']) ->select('dhl_package_shipments.*') // Explicitly select to avoid conflicts ->orderBy('created_at', 'desc'); // Apply filters from the request if ($request->filled('type')) { $query->where('type', $request->get('type')); } if ($request->filled('status')) { $query->where('status', $request->get('status')); } if ($request->filled('date_from')) { $query->whereDate('created_at', '>=', $request->get('date_from')); } if ($request->filled('date_to')) { $query->whereDate('created_at', '<=', $request->get('date_to')); } if ($request->filled('search')) { $search = $request->get('search'); $query->where(function ($q) use ($search) { // Search in shipment fields $q->where('order_id', 'LIKE', "%{$search}%") ->orWhere('dhl_shipment_no', 'LIKE', "%{$search}%") ->orWhere('routing_code', 'LIKE', "%{$search}%") ->orWhere('related_shipment_id', 'LIKE', "%{$search}%") ->orWhere('billing_number', 'LIKE', "%{$search}%") ->orWhere('firstname', 'LIKE', "%{$search}%") ->orWhere('lastname', 'LIKE', "%{$search}%") ->orWhere('company', 'LIKE', "%{$search}%"); }); } return DataTables::eloquent($query) ->addColumn('checkbox', function ($shipment) { return ''; }) ->editColumn('id', function ($shipment) { return '#' . $shipment->id . ''; }) ->addColumn('type', function ($shipment) { if ($shipment->type == 'outbound') { return ' Ausgehend'; } else { return ' Retoure'; } }) ->addColumn('order', function ($shipment) { if ($shipment->order_id) { return '#' . $shipment->order_id . ''; } return 'N/A'; }) ->addColumn('customer', function ($shipment) { return $shipment->firstname . ' ' . $shipment->lastname; }) ->editColumn('dhl_shipment_no', function ($shipment) { return $shipment->dhl_shipment_no ? '' . e($shipment->dhl_shipment_no) . '' : '-'; }) ->addColumn('status', function ($shipment) { $statusMap = [ 'pending' => ['class' => 'warning', 'text' => 'Wartend'], 'created' => ['class' => 'success', 'text' => 'Erstellt'], 'shipped' => ['class' => 'primary', 'text' => 'Versendet'], 'delivered' => ['class' => 'info', 'text' => 'Zugestellt'], 'cancelled' => ['class' => 'secondary', 'text' => 'Storniert'], 'failed' => ['class' => 'danger', 'text' => 'Fehler'], ]; $statusInfo = $statusMap[$shipment->status] ?? ['class' => 'light', 'text' => e($shipment->status)]; return '' . $statusInfo['text'] . ''; }) ->addColumn('tracking_status', function ($shipment) { if ($shipment->tracking_status) { return '' . e($shipment->tracking_status) . '' . ($shipment->last_tracked_at ? '
' . $shipment->last_tracked_at->format('d.m.Y H:i') . '' : ''); } return '-'; }) ->editColumn('weight_kg', function ($shipment) { return number_format($shipment->weight_kg, 2) . ' kg'; }) ->editColumn('created_at', function ($shipment) { return $shipment->created_at->format('d.m.Y H:i'); }) ->addColumn('actions', function ($shipment) { $buttons = '
'; $buttons .= ''; if ($shipment->label_path) { $buttons .= ''; } /* Todo: Add tracking button if ($shipment->canCancel()) { $buttons .= ''; } if ($shipment->type == 'outbound' && !$shipment->returns()->count()) { $buttons .= ''; } */ $buttons .= '
'; return $buttons; }) ->rawColumns(['checkbox', 'id', 'type', 'order', 'customer', 'dhl_shipment_no', 'status', 'tracking_status', 'actions']) ->make(true); } /** * Show the form for creating a new shipment * * @param ShoppingOrder $order * @return View */ public function create(ShoppingOrder $order): View|\Illuminate\Http\RedirectResponse { // Check if order already has a shipment $existingShipment = DhlShipment::where('shopping_order_id', $order->id) ->where('type', 'outbound') ->first(); if ($existingShipment) { return redirect()->route('admin.dhl.show', $existingShipment) ->with('warning', 'Für diese Bestellung existiert bereits eine Sendung.'); } return view('admin.dhl.create', compact('order')); } /** * Store a new shipment (async via queue) * * @param Request $request * @return JsonResponse */ public function store(Request $request): JsonResponse { try { // Use DhlModalService for validation $dhlModalService = new DhlModalService(); $validationResult = $dhlModalService->validateShipmentData($request->all()); if (!$validationResult['valid']) { return response()->json([ 'success' => false, 'message' => 'Validierungsfehler: ' . implode(', ', $validationResult['errors']) ], 422); } // Basic Laravel validation as fallback $request->validate([ 'order_id' => 'required|exists:shopping_orders,id', 'weight' => 'required|numeric|min:0.1|max:31.5', 'product_code' => 'sometimes|string', 'priority' => 'sometimes|string|in:normal,high', 'auto_track' => 'sometimes|boolean', // Shipping address validation 'shipping_firstname' => 'required|string|max:50', 'shipping_lastname' => 'required|string|max:50', 'shipping_company' => 'nullable|string|max:100', 'shipping_address' => 'required|string|max:100', 'shipping_houseNumber' => 'required|string|max:50', 'shipping_zipcode' => 'required|string|max:10', 'shipping_city' => 'required|string|max:50', 'shipping_country_id' => 'required|exists:countries,id', 'shipping_phone' => 'nullable|string|max:20', ]); $order = ShoppingOrder::findOrFail($request->order_id); // Check if shipment already exists /* $existingShipment = DhlShipment::where('shopping_order_id', $order->id) ->where('type', 'outbound') ->first(); if ($existingShipment) { return response()->json([ 'success' => false, 'message' => 'Für diese Bestellung existiert bereits eine Sendung.' ], 422); } */ // Use service to prepare address data $shippingAddress = $dhlModalService->prepareAddressForApi($request->all()); // Prepare options for shipment creation $options = [ 'product_code' => $request->get('product_code', 'V01PAK'), 'priority' => $request->get('priority', 'normal'), 'auto_track' => $request->get('auto_track', true), 'shipping_address' => $shippingAddress, 'services' => $request->get('services', []), 'dimensions' => $request->only(['length', 'width', 'height']) ]; // Use DhlShipmentService (handles queue/sync automatically based on config) $dhlShipmentService = new DhlShipmentService(); $result = $dhlShipmentService->createShipment($order, (float) $request->weight, $options); Log::info('[DHL Controller] Shipment creation processed', [ 'order_id' => $order->id, 'weight' => $request->weight, 'queued' => $result['queued'] ?? false, 'success' => $result['success'] ?? false, ]); return response()->json($result); } catch (Exception $e) { Log::error('[DHL Controller] Failed to dispatch shipment creation', [ 'error' => $e->getMessage(), 'order_id' => $request->order_id ?? 'unknown', ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Erstellen der Sendung: ' . $e->getMessage() ], 500); } } /** * Display the specified shipment * * @param DhlShipment $shipment * @return View */ public function show(DhlShipment $shipment): View { $shipment->load(['shoppingOrder.shopping_user', 'relatedShipment']); return view('admin.dhl.show', compact('shipment')); } /** * Cancel the specified shipment * * @param Request $request * @param DhlShipment $shipment * @return JsonResponse */ public function cancel(Request $request, DhlShipment $shipment): JsonResponse { try { // Validate cancellation is possible if (!$shipment->canCancel()) { return response()->json([ 'success' => false, 'message' => 'Diese Sendung kann nicht mehr storniert werden.' ], 422); } // Dispatch cancellation job $options = [ 'priority' => $request->get('priority', 'normal') ]; CancelShipmentJob::dispatch($shipment, $options); Log::info('[DHL Controller] Shipment cancellation job dispatched', [ 'shipment_id' => $shipment->id, 'shipment_number' => $shipment->shipment_number, ]); return response()->json([ 'success' => true, 'message' => 'Sendung wird storniert...' ]); } catch (Exception $e) { Log::error('[DHL Controller] Failed to dispatch shipment cancellation', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Stornieren der Sendung: ' . $e->getMessage() ], 500); } } /** * Create return label for the specified shipment * * @param Request $request * @param DhlShipment $shipment * @return JsonResponse */ public function createReturnLabel(Request $request, DhlShipment $shipment): JsonResponse { try { // Validate return label creation is possible if ($shipment->type !== 'outbound') { return response()->json([ 'success' => false, 'message' => 'Retourenlabels können nur für ausgehende Sendungen erstellt werden.' ], 422); } // Check if return label already exists $existingReturn = DhlShipment::where('related_shipment_id', $shipment->id) ->where('type', 'return') ->first(); if ($existingReturn) { return response()->json([ 'success' => false, 'message' => 'Für diese Sendung existiert bereits ein Retourenlabel.' ], 422); } // Dispatch return label creation job $options = [ 'auto_track' => $request->get('auto_track', false), 'priority' => $request->get('priority', 'normal') ]; CreateReturnLabelJob::dispatch($shipment, $options); Log::info('[DHL Controller] Return label creation job dispatched', [ 'original_shipment_id' => $shipment->id, 'shipment_number' => $shipment->shipment_number, ]); return response()->json([ 'success' => true, 'message' => 'Retourenlabel wird erstellt...' ]); } catch (Exception $e) { Log::error('[DHL Controller] Failed to dispatch return label creation', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Erstellen des Retourenlabels: ' . $e->getMessage() ], 500); } } /** * Update tracking status for the specified shipment * * @param DhlShipment $shipment * @return JsonResponse */ public function updateTracking(DhlShipment $shipment): JsonResponse { try { if (!$shipment->dhl_shipment_no) { return response()->json([ 'success' => false, 'message' => 'Keine DHL-Sendungsnummer verfügbar.' ], 422); } // Use DhlTrackingService (handles queue/sync automatically based on config) $dhlTrackingService = new DhlTrackingService(); $result = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]); Log::info('[DHL Controller] Tracking update processed', [ 'shipment_id' => $shipment->id, 'dhl_shipment_no' => $shipment->dhl_shipment_no, 'queued' => $result['queued'] ?? false, 'success' => $result['success'] ?? false, ]); return response()->json($result); } catch (Exception $e) { Log::error('[DHL Controller] Failed to process tracking update', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Aktualisieren der Tracking-Informationen: ' . $e->getMessage() ], 500); } } /** * Download shipping label * * @param DhlShipment $shipment * @return Response */ public function downloadLabel(DhlShipment $shipment): Response { try { if (!$shipment->label_path || !Storage::exists($shipment->label_path)) { abort(404, 'Versandlabel nicht gefunden.'); } $labelContent = Storage::get($shipment->label_path); // Generate descriptive filename $filename = $this->generateLabelFilename($shipment); return response($labelContent, 200) ->header('Content-Type', 'application/pdf') ->header('Content-Disposition', "attachment; filename=\"{$filename}\""); } catch (Exception $e) { Log::error('[DHL Controller] Failed to download label', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, 'label_path' => $shipment->label_path, ]); abort(500, 'Fehler beim Download des Versandlabels.'); } } /** * Generate descriptive filename for DHL label * Format: DHL-Kundenname-Sendungsnummer-Datum.pdf * Example: DHL-Geraldine-Seebacher-0034043333301020015589177-15092025.pdf * * @param DhlShipment $shipment * @return string */ private function generateLabelFilename(DhlShipment $shipment): string { // Load order with customer data $customerName = $shipment->firstname . '_' . $shipment->lastname; if ($shipment->company) { $customerName = $shipment->company; } // Clean customer name for filename (remove special characters) $customerName = preg_replace('/[^a-zA-Z0-9\-]/', '', $customerName); $customerName = preg_replace('/-+/', '-', $customerName); // Remove multiple dashes $customerName = trim($customerName, '-'); // Remove leading/trailing dashes // Get shipment number $shipmentNumber = $shipment->dhl_shipment_no ?: $shipment->id; // Get creation date $date = $shipment->created_at->format('d_m_Y'); // Build filename $filename = sprintf( 'DHL-%s-%s-%s.pdf', $customerName, $shipmentNumber, $date ); // Ensure filename is not too long (max 255 characters) if (strlen($filename) > 255) { $maxCustomerLength = 255 - strlen('DHL--' . $shipmentNumber . '-' . $date . '.pdf'); $customerName = substr($customerName, 0, max(10, $maxCustomerLength)); $filename = sprintf( 'DHL-%s-%s-%s.pdf', $customerName, $shipmentNumber, $date ); } return $filename; } /** * Batch operations (multiple shipments) * * @param Request $request * @return JsonResponse|BinaryFileResponse */ public function batchAction(Request $request) { try { $request->validate([ 'action' => 'required|in:cancel,download_labels,update_tracking', 'shipment_ids' => 'required|array|min:1', 'shipment_ids.*' => 'exists:dhl_package_shipments,id', ]); $shipmentIds = $request->shipment_ids; $action = $request->action; $processed = 0; $errors = []; $labels = []; // For batch label download foreach ($shipmentIds as $shipmentId) { try { $shipment = DhlShipment::findOrFail($shipmentId); switch ($action) { case 'cancel': if ($shipment->canCancel()) { CancelShipmentJob::dispatch($shipment); $processed++; } else { $errors[] = "Sendung {$shipment->shipment_number} kann nicht storniert werden."; } break; case 'update_tracking': if ($shipment->dhl_shipment_no) { $dhlTrackingService = new DhlTrackingService(); $trackingResult = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]); if ($trackingResult['success']) { $processed++; } else { $errors[] = "Sendung #{$shipment->id}: " . $trackingResult['message']; } } else { $errors[] = "Sendung #{$shipment->id} hat keine DHL-Sendungsnummer."; } break; case 'download_labels': if ($shipment->label_path && Storage::exists($shipment->label_path)) { $labels[] = [ 'shipment' => $shipment, 'filename' => $this->generateLabelFilename($shipment), 'path' => $shipment->label_path ]; $processed++; } else { $errors[] = "Sendung #{$shipment->id} hat kein verfügbares Label."; } break; } } catch (Exception $e) { $errors[] = "Fehler bei Sendung {$shipmentId}: " . $e->getMessage(); } } // Handle batch label download if ($action === 'download_labels' && !empty($labels)) { return $this->createLabelsZip($labels); } Log::info('[DHL Controller] Batch action executed', [ 'action' => $action, 'processed' => $processed, 'errors_count' => count($errors), ]); return response()->json([ 'success' => $processed > 0, 'message' => sprintf('%d Sendungen verarbeitet.', $processed), 'processed' => $processed, 'errors' => $errors, ]); } catch (Exception $e) { Log::error('[DHL Controller] Batch action failed', [ 'error' => $e->getMessage(), 'action' => $request->action ?? 'unknown', ]); return response()->json([ 'success' => false, 'message' => 'Fehler bei der Stapelverarbeitung: ' . $e->getMessage() ], 500); } } /** * Public tracking page (for customers) * * @param Request $request * @return View|JsonResponse */ public function track(Request $request): View|JsonResponse { if ($request->expectsJson()) { $request->validate([ 'tracking_number' => 'required|string|min:10', ]); try { $shipment = DhlShipment::where('dhl_shipment_no', $request->tracking_number)->first(); if (!$shipment) { return response()->json([ 'success' => false, 'message' => 'Sendung nicht gefunden.' ], 404); } // Use DhlTrackingService for tracking update $dhlTrackingService = new DhlTrackingService(); $trackingResult = $dhlTrackingService->updateTracking($shipment, ['auto_retrack' => false]); return response()->json([ 'success' => $trackingResult['success'], 'message' => $trackingResult['message'], 'data' => [ 'dhl_shipment_no' => $shipment->dhl_shipment_no, 'status' => $shipment->status, 'tracking_status' => $shipment->tracking_status, 'last_tracked_at' => $shipment->last_tracked_at?->format('d.m.Y H:i'), ] ]); } catch (Exception $e) { Log::error('[DHL Controller] Public tracking failed', [ 'error' => $e->getMessage(), 'tracking_number' => $request->tracking_number ?? 'unknown', ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Abrufen der Tracking-Informationen.' ], 500); } } return view('public.tracking'); } /** * Create ZIP file with multiple labels * * @param array $labels Array of label data * @return Response|BinaryFileResponse */ private function createLabelsZip(array $labels) { try { $zip = new ZipArchive(); $zipFilename = 'dhl_labels_' . date('Y-m-d_H-i-s') . '.zip'; $zipPath = storage_path('app/temp/' . $zipFilename); // Ensure temp directory exists if (!file_exists(storage_path('app/temp'))) { mkdir(storage_path('app/temp'), 0755, true); } if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) { throw new Exception('ZIP-Datei konnte nicht erstellt werden.'); } $addedFiles = 0; foreach ($labels as $labelData) { $shipment = $labelData['shipment']; $filename = $labelData['filename']; $filePath = $labelData['path']; if (Storage::exists($filePath)) { $content = Storage::get($filePath); $zip->addFromString($filename, $content); $addedFiles++; } } $zip->close(); if ($addedFiles === 0) { throw new Exception('Keine Labels konnten zur ZIP-Datei hinzugefügt werden.'); } Log::info('[DHL Controller] Labels ZIP created', [ 'zip_file' => $zipFilename, 'files_count' => $addedFiles, 'total_labels' => count($labels) ]); return response()->download($zipPath, $zipFilename)->deleteFileAfterSend(true); } catch (Exception $e) { Log::error('[DHL Controller] Failed to create labels ZIP', [ 'error' => $e->getMessage(), 'labels_count' => count($labels) ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Erstellen der ZIP-Datei: ' . $e->getMessage() ], 500); } } }