hasExportData()) { throw new \RuntimeException('Draft is missing required travel IDs for export'); } $travel = $this->travelDataService->getTravelData( $draft->getDateId(), $draft->getHotelId() ); if (null === $travel) { throw new \RuntimeException('Could not load travel data for export'); } $lookups = $this->buildLookups($travel); $spreadsheet = $this->createSpreadsheet($draft, $lookups); $filename = sprintf( 'buchung_%d_%s.xlsx', $draft->getBookingNumber() ?? $draft->getBookingId(), $draft->getTravelDate()->format('Y-m-d') ); $response = new StreamedResponse(function () use ($spreadsheet): void { $writer = new Xlsx($spreadsheet); $writer->save('php://output'); }); $response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $filename)); $response->headers->set('Cache-Control', 'max-age=0'); return $response; } /** * Builds lookup arrays for converting IDs to labels. * * @param \App\BusProNet\Model\Travel $travel The travel data * * @return array{services: array, rooms: array, pickups: array} */ private function buildLookups(\App\BusProNet\Model\Travel $travel): array { $services = []; $rooms = []; $pickups = []; foreach ($travel->additionalServices as $service) { if (null !== $service->id && null !== $service->label) { $services[$service->id] = $service->label; } } foreach ($travel->transportationServices as $service) { if (null !== $service->id && null !== $service->label) { $services[$service->id] = $service->label; } } foreach ($travel->rooms as $room) { if (null !== $room->id && null !== $room->label) { $rooms[$room->id] = $room->label; } } foreach ($travel->pickups as $pickup) { if (null !== $pickup->id) { $pickups[$pickup->id] = $pickup->getLabel(); } } foreach ($travel->dropOffs as $pickup) { if (null !== $pickup->id) { $pickups[$pickup->id] = $pickup->getLabel(); } } return [ 'services' => $services, 'rooms' => $rooms, 'pickups' => $pickups, ]; } /** * Creates the spreadsheet with participant data. * * @param BookingEditDraft $draft The draft containing form data * @param array $lookups The ID to label lookup arrays */ private function createSpreadsheet(BookingEditDraft $draft, array $lookups): Spreadsheet { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Teilnehmer'); // Write headers foreach (self::COLUMN_HEADERS as $col => $header) { $sheet->setCellValue([$col + 1, 1], $header); } // Style header row $headerStyle = [ 'font' => ['bold' => true], 'fill' => [ 'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, 'startColor' => ['rgb' => 'E0E0E0'], ], ]; $lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(count(self::COLUMN_HEADERS)); $sheet->getStyle('A1:'.$lastColumn.'1')->applyFromArray($headerStyle); // Write participant data $formData = $draft->getFormData(); $participants = $formData['participants'] ?? []; $row = 2; foreach ($participants as $participant) { $this->writeParticipantRow($sheet, $row, $participant, $lookups); ++$row; } // Auto-size columns foreach (range(1, count(self::COLUMN_HEADERS)) as $col) { $colLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col); $sheet->getColumnDimension($colLetter)->setAutoSize(true); } return $spreadsheet; } /** * Writes a single participant row to the spreadsheet. */ private function writeParticipantRow( \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, int $row, array $participant, array $lookups, ): void { $personal = $participant['personalData'] ?? []; $address = $participant['address'] ?? []; $body = $participant['bodyDimensions'] ?? []; $room = $participant['roomAssignment'] ?? []; $services = $participant['services'] ?? []; $vouchers = $participant['vouchers'] ?? []; $data = [ $personal['firstName'] ?? '', $personal['lastName'] ?? '', $this->formatDate($personal['dateOfBirth'] ?? null), $personal['email'] ?? '', $personal['mobile'] ?? '', $this->mapGender($personal['gender'] ?? null), $personal['nationality'] ?? '', $address['street'] ?? '', $address['postCode'] ?? '', $address['city'] ?? '', $address['country'] ?? '', $body['height'] ?? '', $body['weight'] ?? '', $body['shoeSize'] ?? '', $this->resolveRoom($room['assignedRoomId'] ?? null, $lookups['rooms']), $room['remarksRoom'] ?? '', $participant['licensePlate'] ?? '', $this->resolveService($services['skiPass'] ?? null, $lookups['services']), $this->resolveServiceArray($services['courses'] ?? [], $lookups['services']), $this->resolveServiceArray($services['board'] ?? [], $lookups['services']), $this->resolveServiceArray($services['rentals'] ?? [], $lookups['services']), $this->resolveService($services['rentalInsurance'] ?? null, $lookups['services']), $this->resolveServiceArray($services['additionalServices'] ?? [], $lookups['services']), $this->resolveService($services['transportationOutbound'] ?? null, $lookups['services']), $this->resolveService($services['transportationInbound'] ?? null, $lookups['services']), $this->resolvePickup($services['pickup'] ?? null, $lookups['pickups']), $this->resolvePickup($services['dropOff'] ?? null, $lookups['pickups']), $this->formatBoolean($services['parking'] ?? false), $this->resolveService($services['insurance'] ?? null, $lookups['services']), $this->formatBoolean($services['bulkInsuranceBooking'] ?? false), $vouchers['purchaseVoucherCode'] ?? '', $vouchers['promoVoucherCode'] ?? '', ]; foreach ($data as $col => $value) { $sheet->setCellValue([$col + 1, $row], $value); } } /** * Formats a date string to German format. */ private function formatDate(?string $date): string { if (null === $date || '' === $date) { return ''; } try { $dateTime = new \DateTimeImmutable($date); return $dateTime->format('d.m.Y'); } catch (\Exception) { return $date; } } /** * Maps gender code to German label. */ private function mapGender(?string $gender): string { return match ($gender) { 'M' => 'männlich', 'W' => 'weiblich', 'D' => 'divers', default => $gender ?? '', }; } /** * Formats a boolean value to German text. */ private function formatBoolean(bool $value): string { return $value ? 'Ja' : 'Nein'; } /** * Resolves a single service ID to its label. */ private function resolveService(int|string|null $serviceId, array $lookup): string { if (null === $serviceId || '' === $serviceId) { return ''; } $id = (int) $serviceId; return $lookup[$id] ?? (string) $serviceId; } /** * Resolves an array of service IDs to labels, joined by pipe. */ private function resolveServiceArray(array $serviceIds, array $lookup): string { if (true === empty($serviceIds)) { return ''; } $labels = []; foreach ($serviceIds as $serviceId) { if (null !== $serviceId && '' !== $serviceId) { $id = (int) $serviceId; $labels[] = $lookup[$id] ?? (string) $serviceId; } } return implode(', ', $labels); } /** * Resolves a room ID to its label. */ private function resolveRoom(?int $roomId, array $lookup): string { if (null === $roomId) { return ''; } return $lookup[$roomId] ?? (string) $roomId; } /** * Resolves a pickup ID to its label. */ private function resolvePickup(?int $pickupId, array $lookup): string { if (null === $pickupId) { return ''; } return $lookup[$pickupId] ?? (string) $pickupId; } }