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! '.config('dhl.config_source').' '.$dhlConfig['base_url'], '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) */ 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. */ 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')) { $status = DhlShipment::normalizeStatus($request->get('status')); if ($status === 'canceled') { $query->whereIn('status', ['canceled', 'cancelled']); } else { $query->where('status', $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) { $class = $shipment->type === 'return' ? 'text-warning font-weight-bold' : 'text-primary font-weight-semibold'; $icon = $shipment->type === 'return' ? '' : ''; return ''.$icon.'#'.$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 e(trim($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'], 'canceled' => ['class' => 'secondary', 'text' => 'Storniert'], 'failed' => ['class' => 'danger', 'text' => 'Fehler'], ]; $status = DhlShipment::normalizeStatus($shipment->status); $statusInfo = $statusMap[$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 .= ''; } // Email button if ($shipment->dhl_shipment_no && $shipment->canSendTrackingEmail()) { $emailTitle = $shipment->wasTrackingEmailSent() ? 'Tracking-E-Mail erneut senden (gesendet: '.$shipment->tracking_email_sent_at->format('d.m.Y H:i').')' : 'Tracking-E-Mail senden'; $emailClass = $shipment->wasTrackingEmailSent() ? 'btn-success' : 'btn-outline-info'; $buttons .= ''; } // Cancel button if ($shipment->canCancel()) { $buttons .= ''; } // Return label button (for outbound shipments without existing return) if ($shipment->type == 'outbound' && ! $shipment->returns()->count()) { $buttons .= ''; } $buttons .= '
'; return $buttons; }) ->addColumn('DT_RowClass', function ($shipment) { return $shipment->type === 'return' ? 'return-shipment' : ''; }) ->rawColumns(['checkbox', 'id', 'type', 'order', 'customer', 'dhl_shipment_no', 'status', 'tracking_status', 'actions']) ->make(true); } /** * Show the form for creating a new shipment * * @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) */ 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', 'reference' => 'nullable|string|max:35', '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', 'shipping_email' => 'required|email|max:100', 'shipping_postnumber' => 'nullable|string|max:20', ]); $order = ShoppingOrder::findOrFail($request->order_id); $shipmentWeight = max( (float) $request->weight, (new DhlShipmentWeightCalculator)->calculate($order) ); // 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'), 'reference' => $request->get('reference'), '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, $shipmentWeight, $options); Log::info('[DHL Controller] Shipment creation processed', [ 'order_id' => $order->id, 'weight' => $shipmentWeight, 'requested_weight' => $request->weight, 'queued' => $result['queued'] ?? false, 'success' => $result['success'] ?? false, ]); if (! ($result['success'] ?? false)) { return response()->json($result, ($result['type'] ?? null) === 'dhl_address_validation' ? 422 : 500); } 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); } } public function validateAddress(Request $request, DhlAddressValidator $validator): JsonResponse { $country = $request->filled('shipping_country_id') ? Country::find($request->get('shipping_country_id')) : null; $resolver = new DhlProductResolver; $result = $validator->validate(array_merge($request->all(), [ 'shipping_country_code' => $country?->code, ])); $errors = $result['errors']; $warnings = $result['warnings']; $product = [ 'code' => $request->get('product_code'), 'scope' => null, 'scope_label' => 'Nicht geprüft', 'country_code' => $country?->code, 'country_label' => $country?->getLocated(), ]; if ($country) { try { $resolvedProductCode = $resolver->resolveProductCode( $country->code, $request->get('product_code'), config('dhl.default_product', 'V01PAK') ); $product = [ 'code' => $resolvedProductCode, 'scope' => $resolver->getProductScope($resolvedProductCode), 'scope_label' => $resolver->getProductScopeLabel($resolvedProductCode), 'country_code' => $country->code, 'country_label' => $country->getLocated(), ]; } catch (\InvalidArgumentException $e) { $errors[] = $e->getMessage(); } } $status = 'valid'; if ($errors !== []) { $status = 'error'; } elseif ($warnings !== []) { $status = 'warning'; } return response()->json([ 'success' => $errors === [], 'status' => $status, 'can_create_label' => $errors === [], 'errors' => array_values(array_unique($errors)), 'warnings' => array_values(array_unique($warnings)), 'message' => $this->addressValidationMessage($status), 'preflight' => [ 'product' => $product, 'address' => [ 'status' => $result['status'], 'normalized' => $result['normalized'], 'validation_available' => $result['validation_available'], 'validation_level' => $result['validation_level'], 'validation_message' => $result['validation_message'], ], ], ], $errors === [] ? 200 : 422); } private function addressValidationMessage(string $status): string { return match ($status) { 'valid' => 'Adresse ist formal versandfähig.', 'warning' => 'Adresse ist formal versandfähig, sollte aber vor der Labelerstellung geprüft werden.', default => 'Adresse ist nicht versandfähig. Bitte korrigieren Sie die markierten Felder.', }; } /** * Display the specified shipment */ public function show(DhlShipment $shipment): View { $shipment->load([ 'shoppingOrder.shopping_user', 'relatedShipment', 'trackingEvents' => fn ($q) => $q->orderBy('event_time', 'desc'), ]); return view('admin.dhl.show', compact('shipment')); } /** * Cancel the specified shipment */ 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); } // Use DhlShipmentService (handles queue/sync automatically based on config) $options = [ 'priority' => $request->get('priority', 'normal'), ]; $dhlShipmentService = new DhlShipmentService; $result = $dhlShipmentService->cancelShipment($shipment, $options); Log::info('[DHL Controller] Shipment cancellation 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 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 */ 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); } // Check DHL_USE_QUEUE configuration $settingController = new SettingController; $dhlConfig = $settingController->getDhlConfig(); $useQueue = $dhlConfig['use_queue'] ?? false; if ($useQueue) { // 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->dhl_shipment_no, ]); return response()->json([ 'success' => true, 'message' => 'Retourenlabel wird im Hintergrund erstellt. Dies kann einige Sekunden dauern.', ]); } else { // Create synchronously $result = $this->createReturnLabelSync($shipment); return response()->json($result); } } catch (Exception $e) { Log::error('[DHL Controller] Failed to create return label', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Erstellen des Retourenlabels: '.$e->getMessage(), ], 500); } } /** * Get billing address for return label (used when original delivery was to Packstation) */ private function getBillingAddressForReturn($shippingUser, array $recipient): array { if (! $shippingUser) { Log::warning('[DHL Controller] No shipping user found, using recipient country only', [ 'order_recipient_country' => $recipient['country'] ?? null, ]); // Fallback: use recipient data but without Packstation fields. // We keep ISO-2 country codes here so that ReturnsService / // DhlProductResolver can normalize / validate them consistently. return [ 'name' => trim(($recipient['firstname'] ?? '').' '.($recipient['lastname'] ?? '')), 'name2' => $recipient['company'] ?? '', 'street' => 'Adresse fehlt', 'houseNumber' => '', 'postalCode' => $recipient['postalCode'] ?? '', 'city' => $recipient['city'] ?? '', 'country' => $recipient['country'] ?? DhlProductResolver::DOMESTIC_COUNTRY, 'email' => $recipient['email'] ?? '', 'phone' => $recipient['phone'] ?? '', ]; } // Parse billing address to extract street and house number $billingAddress = trim($shippingUser->billing_address ?? ''); $street = $billingAddress; $houseNumber = ''; // Try to extract house number from address if (preg_match('/^(.+?)\s+(\d+[a-zA-Z]?[-\/\d]*)$/u', $billingAddress, $matches)) { $street = trim($matches[1]); $houseNumber = trim($matches[2]); } return [ 'name' => trim(($shippingUser->billing_firstname ?? '').' '.($shippingUser->billing_lastname ?? '')), 'name2' => $shippingUser->billing_company ?? '', 'street' => $street, 'houseNumber' => $houseNumber, 'postalCode' => $shippingUser->billing_zipcode ?? '', 'city' => $shippingUser->billing_city ?? '', 'country' => $shippingUser->billing_country?->code ?? DhlProductResolver::DOMESTIC_COUNTRY, 'email' => $shippingUser->billing_email ?? '', 'phone' => $shippingUser->billing_phone ?? '', ]; } /** * Create return label synchronously */ private function createReturnLabelSync(DhlShipment $shipment): array { try { Log::info('[DHL Controller] Creating return label synchronously', [ 'original_shipment_id' => $shipment->id, ]); // Get DHL configuration $settingController = new SettingController; $dhlConfig = $settingController->getDhlConfig(); // Initialize DHL client $dhlClient = new \Acme\Dhl\Support\DhlClient( $dhlConfig['base_url'], $dhlConfig['api_key'], $dhlConfig['username'], $dhlConfig['password'] ); // Use ReturnsService instead of ShippingService $returnsService = new \Acme\Dhl\Services\ReturnsService($dhlClient); // Prepare return label data $order = $shipment->shoppingOrder; $recipient = $shipment->recipient ?? []; // Check if this is a Packstation delivery - use billing address as return sender $hasPostNumber = ! empty($recipient['postnumber'] ?? $recipient['postNumber'] ?? ''); if ($hasPostNumber) { Log::info('[DHL Controller] Packstation detected - using billing address for return sender', [ 'shipment_id' => $shipment->id, 'order_id' => $order->id, ]); // Load billing address from order $shippingUser = $order->shopping_user; $shipperAddress = $this->getBillingAddressForReturn($shippingUser, $recipient); } else { // Use original recipient address (normal delivery) $shipperAddress = [ 'name' => trim(($recipient['firstname'] ?? '').' '.($recipient['lastname'] ?? '')), 'name2' => $recipient['company'] ?? '', 'street' => $recipient['street'] ?? '', 'houseNumber' => $recipient['houseNumber'] ?? '', 'postalCode' => $recipient['postalCode'] ?? '', 'city' => $recipient['city'] ?? '', 'country' => $recipient['country'] ?? DhlProductResolver::DOMESTIC_COUNTRY, 'email' => $recipient['email'] ?? '', 'phone' => $recipient['phone'] ?? '', ]; } $returnData = [ 'order_id' => $order->id, 'original_shipment_id' => $shipment->id, 'weight_kg' => $shipment->weight_kg, 'label_format' => $shipment->label_format ?? 'PDF', // Shipper: Customer sends back to us (using billing address for Packstation) 'shipper' => $shipperAddress, // Consignee: Our warehouse 'consignee' => [ 'name' => $dhlConfig['sender']['company'] ?? 'mivita care gmbh', 'name2' => $dhlConfig['sender']['name'] ?? '', 'street' => $dhlConfig['sender']['street'] ?? 'Leinfeld', 'houseNumber' => $dhlConfig['sender']['house_number'] ?? '2', 'postalCode' => $dhlConfig['sender']['postalCode'] ?? '87755', 'city' => $dhlConfig['sender']['city'] ?? 'Kirchhaslach', 'country' => $dhlConfig['sender']['country'] ?? DhlProductResolver::DOMESTIC_COUNTRY, 'email' => $dhlConfig['sender']['email'] ?? 'versand@mivita.care', 'phone' => $dhlConfig['sender']['phone'] ?? '+49 123 456789', ], ]; // Create the return label using ReturnsService $result = $returnsService->createReturn($returnData); Log::info('[DHL Controller] Return label created successfully (sync)', [ 'original_shipment_id' => $shipment->id, 'return_shipment_number' => $result['returnNumber'] ?? 'N/A', ]); return [ 'success' => true, 'message' => 'Retourenlabel wurde erfolgreich erstellt!', 'shipment_number' => $result['returnNumber'] ?? null, 'return_shipment' => $result['returnShipment'] ?? null, ]; } catch (Exception $e) { Log::error('[DHL Controller] Return label creation failed (sync)', [ 'original_shipment_id' => $shipment->id, 'error' => $e->getMessage(), ]); return [ 'success' => false, 'message' => 'Fehler beim Erstellen des Retourenlabels: '.$e->getMessage(), ]; } } /** * Update tracking status for the specified shipment */ 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); } } /** * Send tracking email to customer (supports multiple shipments per order) */ public function sendTrackingEmail(DhlShipment $shipment): JsonResponse { try { // Check if shipment has tracking number if (! $shipment->dhl_shipment_no) { return response()->json([ 'success' => false, 'message' => 'Keine DHL-Sendungsnummer verfügbar.', ], 422); } // Check if shipment can send email if (! $shipment->canSendTrackingEmail()) { return response()->json([ 'success' => false, 'message' => 'E-Mail kann nicht gesendet werden. Bestellung oder E-Mail-Adresse fehlt.', ], 422); } $order = $shipment->shoppingOrder; // Determine recipient email: prefer shipment email, fallback to shopping user email $recipientEmail = null; if (! empty($shipment->email)) { $recipientEmail = $shipment->email; } elseif ($order->shopping_user && ! empty($order->shopping_user->email)) { $recipientEmail = $order->shopping_user->email; } if (! $recipientEmail) { return response()->json([ 'success' => false, 'message' => 'Keine Empfänger-E-Mail-Adresse verfügbar.', ], 422); } // Collect all shipments for this order that have tracking numbers $allShipments = DhlShipment::where('order_id', $order->id) ->whereNotNull('dhl_shipment_no') ->whereIn('status', ['created', 'in_transit', 'out_for_delivery']) ->orderBy('created_at', 'asc') ->get(); // If no shipments found, use only the current one if ($allShipments->isEmpty()) { $allShipments = collect([$shipment]); } // Send email with all shipments Mail::to($recipientEmail)->send(new MailDhlTracking($allShipments, $order)); // Mark all included shipments as sent foreach ($allShipments as $s) { $s->markTrackingEmailSent('manual', $recipientEmail, $allShipments); } Log::info('[DHL Controller] Tracking email sent', [ 'shipment_ids' => $allShipments->pluck('id')->toArray(), 'shipments_count' => $allShipments->count(), 'dhl_shipment_nos' => $allShipments->pluck('dhl_shipment_no')->toArray(), 'email' => $recipientEmail, 'type' => 'manual', ]); $message = $allShipments->count() > 1 ? "Tracking-E-Mail mit {$allShipments->count()} Sendungen wurde erfolgreich an {$recipientEmail} gesendet." : "Tracking-E-Mail wurde erfolgreich an {$recipientEmail} gesendet."; return response()->json([ 'success' => true, 'message' => $message, 'sent_at' => now()->format('d.m.Y H:i'), 'shipments_count' => $allShipments->count(), ]); } catch (Exception $e) { Log::error('[DHL Controller] Failed to send tracking email', [ 'error' => $e->getMessage(), 'shipment_id' => $shipment->id, ]); return response()->json([ 'success' => false, 'message' => 'Fehler beim Senden der Tracking-E-Mail: '.$e->getMessage(), ], 500); } } /** * Download shipping label */ 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 */ 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) * * @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) */ 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); } } }