feat: replace *Service suffix with role-based class names
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user