Files
myep/src/Service/BookingExporter.php
T

371 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Model\Travel;
use App\Entity\BookingEditDraft;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Generates Excel exports from booking edit drafts.
*
* This service transforms draft booking data into a downloadable Excel file,
* converting all service, room, and pickup IDs to human-readable labels using
* travel data loaded from XML files.
*/
class BookingExporter
{
private const COLUMN_HEADERS = [
'Vorname',
'Nachname',
'Geburtsdatum',
'E-Mail',
'Mobil',
'Geschlecht',
'Nationalität',
'Straße',
'PLZ',
'Ort',
'Land',
'Größe',
'Gewicht',
'Schuhgröße',
'Zimmer',
'Zimmerwünsche',
'Kennzeichen',
'Skipass',
'Kurse',
'Verpflegung',
'Leihmaterial',
'Leihmaterial-Versicherung',
'Zusatzleistungen',
'Hinfahrt',
'Rückfahrt',
'Zustieg',
'Ausstieg',
'Parkplatz',
'Versicherung',
'Sammelversicherung',
'Kaufgutschein',
'Aktionsgutschein',
];
public function __construct(
private readonly TravelDataProvider $travelDataService,
) {
}
/**
* Creates a StreamedResponse with the Excel export for the given draft.
*
* @param BookingEditDraft $draft The draft to export
*
* @return StreamedResponse The response containing the Excel file
*
* @throws \RuntimeException If travel data cannot be loaded
*/
public function createExportResponse(BookingEditDraft $draft): StreamedResponse
{
if (false === $draft->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 Travel $travel The travel data
*
* @return array{services: array<int, string>, rooms: array<int, string>, pickups: array<int, string>}
*/
private function buildLookups(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<string, array<int, string>> $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' => Fill::FILL_SOLID,
'startColor' => ['rgb' => 'E0E0E0'],
],
];
$lastColumn = 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 = Coordinate::stringFromColumnIndex($col);
$sheet->getColumnDimension($colLetter)->setAutoSize(true);
}
return $spreadsheet;
}
/**
* Writes a single participant row to the spreadsheet.
*/
/**
* @param array<string, mixed> $participant
* @param array<string, array<int, string>> $lookups
*/
private function writeParticipantRow(
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.
*/
/** @param array<int, string> $lookup */
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.
*/
/**
* @param list<int|string|null> $serviceIds
* @param array<int, string> $lookup
*/
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.
*/
/** @param array<int, string> $lookup */
private function resolveRoom(?int $roomId, array $lookup): string
{
if (null === $roomId) {
return '';
}
return $lookup[$roomId] ?? (string) $roomId;
}
/**
* Resolves a pickup ID to its label.
*/
/** @param array<int, string> $lookup */
private function resolvePickup(?int $pickupId, array $lookup): string
{
if (null === $pickupId) {
return '';
}
return $lookup[$pickupId] ?? (string) $pickupId;
}
}