356 lines
11 KiB
PHP
356 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\Entity\BookingEditDraft;
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
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 \App\BusProNet\Model\Travel $travel The travel data
|
|
*
|
|
* @return array{services: array<int, string>, rooms: array<int, string>, pickups: array<int, string>}
|
|
*/
|
|
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;
|
|
}
|
|
}
|