wip: first working version

This commit is contained in:
Björn Fromme
2024-11-21 18:38:41 +01:00
parent d4c4e69d09
commit bd13ae6537
24 changed files with 590 additions and 319 deletions
+83 -96
View File
@@ -6,6 +6,7 @@ use App\BusProNet\ApiResponseParser\ResponseParser;
use App\BusProNet\Exception\ApiClientException; use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\BaseData; use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking; use App\BusProNet\Model\Booking;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\CrmAttributes; use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\File; use App\BusProNet\Model\File;
use App\BusProNet\Model\Notification; use App\BusProNet\Model\Notification;
@@ -13,6 +14,7 @@ use App\BusProNet\Model\PersonalData;
use App\Form\Model\BookingData; use App\Form\Model\BookingData;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -44,14 +46,12 @@ class ApiClient
public function getPersonalData(string $email, string $password): Notification|PersonalData public function getPersonalData(string $email, string $password): Notification|PersonalData
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => 'Adressdaten',
'art' => 'Adressdaten', 'email' => $email,
'email' => $email, 'passwort' => $password,
'passwort' => $password,
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
@@ -63,16 +63,14 @@ class ApiClient
public function updatePersonalData(string $email, string $password, PersonalData $personalData): Notification|PersonalData public function updatePersonalData(string $email, string $password, PersonalData $personalData): Notification|PersonalData
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => 'Adressdaten_Ändern',
'art' => 'Adressdaten_Ändern', 'email' => $email,
'email' => $email, 'passwort' => $password,
'passwort' => $password, 'idadresse' => $personalData->addressId,
'idadresse' => $personalData->addressId, 'adressdaten' => $personalData->toPayload(),
'adressdaten' => $personalData->toPayload(),
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
@@ -84,14 +82,12 @@ class ApiClient
public function getBookings(string $email, string $password): Notification|BaseData public function getBookings(string $email, string $password): Notification|BaseData
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => 'Vorgänge',
'art' => 'Vorgänge', 'email' => $email,
'email' => $email, 'passwort' => $password,
'passwort' => $password,
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
@@ -103,33 +99,29 @@ class ApiClient
public function getBooking(string $email, string $password, int $id): Notification|Booking public function getBooking(string $email, string $password, int $id): Notification|Booking
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => 'Vorgang_Details',
'art' => 'Vorgang_Details', 'email' => $email,
'email' => $email, 'passwort' => $password,
'passwort' => $password, 'idbuchung' => $id,
'idbuchung' => $id,
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
} }
public function updateBooking(string $email, string $password, BookingData $formData): Notification public function updateBooking(BookingData $formData, bool $dryRun = true): Notification|BookingUpdate
{ {
$booking = $formData->booking; $booking = $formData->booking;
$mode = $dryRun ? 'Anfrage' : 'Buchung';
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE), 'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE],
'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE], 'buchungsart' => $mode,
'buchungsart' => 'Buchung', ...$booking->toPayload($formData),
'idbuchung' => $booking->id,
...$booking->toPayload($formData),
],
]; ];
return $this->sendRequest(static::TYPE_BOOKING_UPDATE, $data); return $this->sendRequest(static::TYPE_BOOKING_UPDATE, $data);
@@ -141,13 +133,11 @@ class ApiClient
public function getMutableFields(int $id): Notification|BaseData public function getMutableFields(int $id): Notification|BaseData
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_MUTABLE_FIELDS),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_MUTABLE_FIELDS), 'satz' => ['@typ' => static::TYPE_MUTABLE_FIELDS],
'satz' => ['@typ' => static::TYPE_MUTABLE_FIELDS], 'art' => 'Vorgang_Details',
'art' => 'Vorgang_Details', 'idreise' => $id,
'idreise' => $id,
],
]; ];
return $this->sendRequest(static::TYPE_MUTABLE_FIELDS, $data); return $this->sendRequest(static::TYPE_MUTABLE_FIELDS, $data);
@@ -159,12 +149,10 @@ class ApiClient
public function getAvailabilities(int $id): Notification|BaseData public function getAvailabilities(int $id): Notification|BaseData
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AVAILABILITY),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AVAILABILITY), 'satz' => ['@typ' => static::TYPE_AVAILABILITY],
'satz' => ['@typ' => static::TYPE_AVAILABILITY], 'idreise' => $id,
'idreise' => $id,
],
]; ];
return $this->sendRequest(static::TYPE_AVAILABILITY, $data); return $this->sendRequest(static::TYPE_AVAILABILITY, $data);
@@ -176,13 +164,11 @@ class ApiClient
public function resetPassword(string $email): Notification public function resetPassword(string $email): Notification
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => 'Passwort_Anfrage',
'art' => 'Passwort_Anfrage', 'email' => $email,
'email' => $email,
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
@@ -194,14 +180,12 @@ class ApiClient
public function getCrmAttributes(string $email, string $password): Notification|CrmAttributes public function getCrmAttributes(string $email, string $password): Notification|CrmAttributes
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => 'SelektionCRM',
'art' => 'SelektionCRM', 'email' => $email,
'email' => $email, 'passwort' => $password,
'passwort' => $password,
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
@@ -213,11 +197,9 @@ class ApiClient
public function getBaseData(string $type): Notification|BaseData public function getBaseData(string $type): Notification|BaseData
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], $type),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], $type), 'satz' => ['@typ' => $type],
'satz' => ['@typ' => $type],
],
]; ];
return $this->sendRequest($type, $data); return $this->sendRequest($type, $data);
@@ -230,15 +212,13 @@ class ApiClient
public function getDocuments(string $email, string $password, int $id, string $type): mixed public function getDocuments(string $email, string $password, int $id, string $type): mixed
{ {
$data = [ $data = [
'anfrage' => [ 'user' => $this->config['bpn_username'],
'user' => $this->config['bpn_username'], 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA), 'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA], 'art' => $type,
'art' => $type, 'email' => $email,
'email' => $email, 'passwort' => $password,
'passwort' => $password, 'idbuchung' => $id,
'idbuchung' => $id,
],
]; ];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data); return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
@@ -249,18 +229,18 @@ class ApiClient
*/ */
private function sendRequest(string $type, array $data): mixed private function sendRequest(string $type, array $data): mixed
{ {
$requestId = (string) Uuid::v7(); $requestId = Uuid::v7();
$body = $this $body = $this
->serializer ->serializer
->serialize($data, 'xml') ->serialize($data, 'xml', [
XmlEncoder::ROOT_NODE_NAME => 'anfrage',
XmlEncoder::ENCODING => 'UTF-8',
])
; ;
if (true === $this->config['debug']) { if (true === $this->config['debug']) {
$this->logger->info('Request sent', [ $this->dumpXmlToFile('request', $requestId, $body);
'id' => $requestId,
'request' => $body,
]);
} }
try { try {
@@ -275,10 +255,7 @@ class ApiClient
$xml = $response->getContent(); $xml = $response->getContent();
if (true === $this->config['debug']) { if (true === $this->config['debug']) {
$this->logger->info('Response received', [ $this->dumpXmlToFile('response', $requestId, $xml);
'id' => $requestId,
'response' => $xml,
]);
} }
return $this->responseParser->parseXmlString($type, $xml); return $this->responseParser->parseXmlString($type, $xml);
@@ -289,6 +266,15 @@ class ApiClient
throw new ApiClientException($e->getMessage()); throw new ApiClientException($e->getMessage());
} }
private function dumpXmlToFile(string $type, string $requestId, string $body): void
{
if (false === file_exists($this->config['target_folder_dumps'])) {
mkdir($this->config['target_folder_dumps']);
}
file_put_contents($this->config['target_folder_dumps'].'/'.$requestId.'_'.$type.'.xml', $body);
}
private function createKey(string $username, string $password, string $type): string private function createKey(string $username, string $password, string $type): string
{ {
$date = (new \DateTimeImmutable())->format('Ymd'); $date = (new \DateTimeImmutable())->format('Ymd');
@@ -302,6 +288,7 @@ class ApiClient
$optionsResolver->setRequired(['bpn_url', 'bpn_username', 'bpn_password']); $optionsResolver->setRequired(['bpn_url', 'bpn_username', 'bpn_password']);
$optionsResolver->setDefaults([ $optionsResolver->setDefaults([
'debug' => false, 'debug' => false,
'target_folder_dumps' => '/var/www/html/var/bpn',
]); ]);
return $optionsResolver->resolve($options); return $optionsResolver->resolve($options);
@@ -20,6 +20,7 @@ class BookingResponseParser
$booking = new Booking(); $booking = new Booking();
$booking->id = (int) $xml->idbuchung; $booking->id = (int) $xml->idbuchung;
$booking->agencyId = (int) $xml->idagentur;
$booking->bookingNumber = (int) $xml->vorgang; $booking->bookingNumber = (int) $xml->vorgang;
$booking->invoiceNumber = (int) $xml->zahlungsdaten->rechnung; $booking->invoiceNumber = (int) $xml->zahlungsdaten->rechnung;
$booking->totalPrice = $this->stringToFloat((string) $xml->zahlungsdaten->gesamtbetrag); $booking->totalPrice = $this->stringToFloat((string) $xml->zahlungsdaten->gesamtbetrag);
@@ -35,9 +36,16 @@ class BookingResponseParser
$booking->applicant = $this->parsePersonalData($xml->anmelder); $booking->applicant = $this->parsePersonalData($xml->anmelder);
$booking->participantsStatus = $this->stringToArray((string) $xml->status_teilnehmer, '/'); $participantsStatus = $this->stringToArray((string) $xml->status_teilnehmer, '/');
$booking->participantsStatus = $this->arrayToOneBased($participantsStatus);
$booking->participants = $this->parseParticipants($xml); $booking->participants = $this->parseParticipants($xml);
$paymentData = $xml->zahlung;
$booking->paymentId = (int) $paymentData->attributes()['idzahlungsart'];
$booking->paymentLabel = (string) $paymentData->attributes()['bezeichnung'];
$booking->paymentType = (string) $paymentData->attributes()['art'];
if ($xml->beförderungen) { if ($xml->beförderungen) {
$booking->transportationServices = $this $booking->transportationServices = $this
->parseServices($xml->beförderungen->beförderung, Service::TYPE_TRANSPORTATION); ->parseServices($xml->beförderungen->beförderung, Service::TYPE_TRANSPORTATION);
@@ -149,8 +157,10 @@ class BookingResponseParser
$room = new Room(); $room = new Room();
$room->id = $id; $room->id = $id;
$room->label = (string) $attributes['zimmer']; $room->label = (string) $attributes['zimmer'];
$room->dateFrom = $attributes['termin'] ? $this->stringToDate((string) $attributes['anreise']) : null; $room->category = (string) $attributes['kategorie'];
$room->dateTo = $attributes['terminbis'] ? $this->stringToDate((string) $attributes['abreise']) : null; $room->boardId = (int) $attributes['idverpflegung'];
$room->dateFrom = $attributes['anreise'] ? $this->stringToDate((string) $attributes['anreise']) : null;
$room->dateTo = $attributes['abreise'] ? $this->stringToDate((string) $attributes['abreise']) : null;
$room->totalCount = (int) $attributes['anzahl']; $room->totalCount = (int) $attributes['anzahl'];
$room->minPax = (int) $attributes['minpax']; $room->minPax = (int) $attributes['minpax'];
$room->maxPax = (int) $attributes['maxpax']; $room->maxPax = (int) $attributes['maxpax'];
@@ -0,0 +1,19 @@
<?php
namespace App\BusProNet\ApiResponseParser;
use App\BusProNet\Model\BookingUpdate;
class BookingUpdateResponseParser
{
use ResponseParserTrait;
public function parse(\SimpleXMLElement $xml): BookingUpdate
{
$bookingUpdate = new BookingUpdate();
$bookingUpdate->valid = 'möglich' === (string) $xml->aenderung;
$bookingUpdate->totalPrice = $this->stringToFloat((string) $xml->gesamtpreis);
return $bookingUpdate;
}
}
@@ -49,6 +49,8 @@ class ResponseParser
return (new MutableFieldsResponseParser())->parse($xml); return (new MutableFieldsResponseParser())->parse($xml);
case ApiClient::TYPE_AVAILABILITY: case ApiClient::TYPE_AVAILABILITY:
return (new AvailabilitiesResponseParser())->parse($xml); return (new AvailabilitiesResponseParser())->parse($xml);
case ApiClient::TYPE_BOOKING_UPDATE:
return (new BookingUpdateResponseParser())->parse($xml);
} }
throw new ResponseParserException('Unable to parse XML response'); throw new ResponseParserException('Unable to parse XML response');
@@ -166,14 +166,16 @@ class TravelDataLoader extends AbstractDataLoader
$room = new Room(); $room = new Room();
$room->id = $roomId; $room->id = $roomId;
$room->code = (string) $item->attributes()['zimmercode']; $room->code = (string) $attributes['zimmercode'];
$room->label = (string) $item->attributes()['zimmertext']; $room->category = (string) $attributes['kat'];
$room->minPax = (int) $item->attributes()['MinPax']; $room->boardId = (int) $attributes['idbuspro_vp'];
$room->maxPax = (int) $item->attributes()['MaxPax']; $room->label = (string) $attributes['zimmertext'];
$room->nights = (int) $item->attributes()['naechte']; $room->minPax = (int) $attributes['MinPax'];
$room->maxPax = (int) $attributes['MaxPax'];
$room->nights = (int) $attributes['naechte'];
$room->price = $attributes['preis'] ? $this->stringToFloat((string) $attributes['preis']) : null; $room->price = $attributes['preis'] ? $this->stringToFloat((string) $attributes['preis']) : null;
$room->status = (string) $item->status; $room->status = (string) $item->status;
$room->available = (int) $item->attributes()['verfuegbar']; $room->available = (int) $attributes['verfuegbar'];
$rooms[$roomId] = $room; $rooms[$roomId] = $room;
} }
+84 -9
View File
@@ -7,6 +7,7 @@ use App\Form\Model\BookingData;
class Booking class Booking
{ {
public ?int $id = null; public ?int $id = null;
public ?int $agencyId = null;
public ?int $bookingNumber = null; public ?int $bookingNumber = null;
public ?Travel $travelData = null; public ?Travel $travelData = null;
public ?string $status = null; public ?string $status = null;
@@ -22,11 +23,15 @@ class Booking
public ?string $hotelName = null; public ?string $hotelName = null;
public ?bool $document = null; public ?bool $document = null;
public ?float $payment = null; public ?float $payment = null;
public ?string $paymentId = null;
public ?string $paymentType = null;
public ?string $paymentLabel = null;
public array $participantsStatus = []; public array $participantsStatus = [];
public array $participants = []; public array $participants = [];
public array $transportationServices = []; public array $transportationServices = [];
public array $additionalServices = []; public array $additionalServices = [];
public array $rooms = []; public array $rooms = [];
public array $pickups = [];
public ?int $invoiceNumber = null; public ?int $invoiceNumber = null;
public ?float $totalPrice = null; public ?float $totalPrice = null;
@@ -111,11 +116,11 @@ class Booking
public function toPayload(?BookingData $formData): array public function toPayload(?BookingData $formData): array
{ {
// Reset services to participants mappings // Reset mappings
foreach ([...$this->additionalServices, ...$this->transportationServices] as $service) { foreach ([...$this->additionalServices, ...$this->transportationServices, ...$this->pickups] as $service) {
$service->mapping = []; $service->mapping = [];
} }
// Update mappings and add previously unselected services // Update mappings, add services and pickups
foreach ($formData->participants as $participant) { foreach ($formData->participants as $participant) {
$servicesToMap = [ $servicesToMap = [
...$participant->courses, ...$participant->courses,
@@ -140,6 +145,12 @@ class Booking
} }
$this->transportationServices[$service->id]->mapping[] = $participant->index; $this->transportationServices[$service->id]->mapping[] = $participant->index;
} }
if ('BUS' === $participant->transportationServiceTo->subType && null !== $selectedPickup = $participant->pickup) {
if (false === isset($this->pickups[$selectedPickup->id])) {
$this->pickups[$selectedPickup->id] = $selectedPickup;
}
$this->pickups[$selectedPickup->id]->mapping[] = $participant->index;
}
} }
// Remove services with empty mappings // Remove services with empty mappings
foreach ($this->additionalServices as $service) { foreach ($this->additionalServices as $service) {
@@ -166,15 +177,79 @@ class Booking
$this->participants[$participant->index]->communication->mobile = $participant->mobile; $this->participants[$participant->index]->communication->mobile = $participant->mobile;
} }
return [ $payload = [
'idbuchung' => $this->id,
'status' => $this->status, 'status' => $this->status,
'idagentur' => $this->agencyId,
'idreise' => $this->travelId, 'idreise' => $this->travelId,
'idpartner' => $this->hotelId,
'anmelder' => $this->applicant->toPayload(), 'anmelder' => $this->applicant->toPayload(),
'teilnehmerliste' => [], 'zahlung' => [
'beförderungen' => [], '@idzahlungsart' => $this->paymentId,
'unterbringungen' => [], '@bezeichnung' => $this->paymentLabel,
'zusatzleistungen' => [], '@art' => $this->paymentType,
'zustiege' => [], ],
'teilnehmerliste' => [
'teilnehmer' => [],
],
'zusatzleistungen' => [
'zusatzleistung' => [],
],
'beförderungen' => [
'beförderung' => [],
],
'ferienzielunterbringungen' => [
'ferienzielunterbringung' => [],
],
]; ];
foreach ($this->participants as $index => $participant) {
$payload['teilnehmerliste']['teilnehmer'][] = [
'@id' => $index,
'status' => $this->participantsStatus[$index],
...$participant->toPayload(),
];
}
foreach ($this->additionalServices as $service) {
$payload['zusatzleistungen']['zusatzleistung'][] = [
'@idleistung' => $service->id,
'@anzahl' => count($service->mapping),
'@zuordnung' => implode(',', $service->mapping),
];
}
foreach ($this->transportationServices as $service) {
$payload['beförderungen']['beförderung'][] = [
'@idleistung' => $service->id,
'@anzahl' => count($service->mapping),
'@zuordnung' => implode(',', $service->mapping),
];
}
foreach ($this->rooms as $room) {
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
'@idzimmer' => $room->id,
'@kategorie' => $room->category,
'@idverpflegung' => $room->boardId,
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
'@anzahl' => count($room->mapping),
'@zuordnung' => implode(',', $room->mapping),
];
}
if (0 < count($this->pickups)) {
$payload['zustiege']['zustieg'] = [];
foreach ($this->pickups as $pickup) {
$payload['zustiege']['zustieg'][] = [
'@idzustieg' => $pickup->id,
'@anzahl' => count($pickup->mapping),
'@zuordnung' => implode(',', $pickup->mapping),
];
}
}
return $payload;
} }
} }
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App\BusProNet\Model;
class BookingUpdate
{
public bool $valid = false;
public ?string $status = null;
public array $prices = [];
public ?float $totalPrice = null;
}
+3
View File
@@ -44,6 +44,9 @@ class PersonalData
'name' => $this->name, 'name' => $this->name,
'anschrift' => $this->address->toPayload(), 'anschrift' => $this->address->toPayload(),
'kommunikation' => $this->communication->toPayload(), 'kommunikation' => $this->communication->toPayload(),
'sonstiges1' => $this->height,
'sonstiges2' => $this->weight,
'sonstiges3' => $this->shoeSize,
]; ];
} }
} }
+1
View File
@@ -11,4 +11,5 @@ class Pickup
public ?string $street = null; public ?string $street = null;
public ?\DateTimeImmutable $time = null; public ?\DateTimeImmutable $time = null;
public ?float $price = null; public ?float $price = null;
public array $mapping = [];
} }
+2
View File
@@ -5,6 +5,8 @@ namespace App\BusProNet\Model;
class Room class Room
{ {
public ?int $id = null; public ?int $id = null;
public ?string $category = null;
public ?int $boardId = null;
public ?string $code = null; public ?string $code = null;
public ?string $label = null; public ?string $label = null;
public ?\DateTimeImmutable $dateFrom = null; public ?\DateTimeImmutable $dateFrom = null;
@@ -0,0 +1,76 @@
<?php
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\Notification;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use function Symfony\Component\String\u;
class DownloadController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly Security $security,
) {
}
#[Route(
path: '/bookings/{id}/documents',
name: 'app_booking_documents',
requirements: ['id' => '\d+'],
defaults: ['fileType' => 'documents']
)]
#[Route(
path: '/bookings/{id}/confirmation',
name: 'app_booking_confirmation',
requirements: ['id' => '\d+'],
defaults: ['fileType' => 'confirmation']
)]
#[IsGranted("ROLE_USER")]
public function documents(int $id, string $fileType, Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$type = match ($fileType) {
'documents' => 'Dokumentendruck',
'confirmation' => 'Vorgangdruck',
};
$file = $this
->apiClient
->getDocuments($bpnUser->getEmail(), $bpnUser->getPassword(), $id, $type)
;
if (null === $file || $file instanceof Notification) {
$this->addFlash('error', 'Keine Dokumente vorhanden');
return $this->redirectToRoute('app_bookings');
}
$filename = u($file->filename)->ascii();
$response = new Response($file->content);
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$filename,
md5($filename)
);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', $file->mimeType);
return $response;
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\DataLoader\PickupDataLoader;
use App\BusProNet\DataLoader\TravelDataLoader;
use App\Form\BookingType;
use App\Form\Model\BookingData;
use App\Htmx\HxRedirectResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class EditController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelDataLoader $travelDataLoader,
private readonly PickupDataLoader $pickupDataLoader,
private readonly Security $security,
) {
}
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', requirements: ['id' => '\d+'])]
#[IsGranted("ROLE_USER")]
public function edit(int $id, Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$booking = $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
//$this->denyAccessUnlessGranted('VIEW', $booking);
$travelData = $this->travelDataLoader->loadById($booking->travelId);
if (null === $travelData) {
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
return $this->redirectToRoute('app_bookings');
}
$booking->travelData = $travelData;
$mutableFields = $this->apiClient->getMutableFields($booking->travelId);
$availabilities = $this->apiClient->getAvailabilities($booking->travelId);
$this->travelDataLoader->enrichServicesData($travelData, $availabilities);
$this->pickupDataLoader->enrichPickupsData($travelData);
$formData = BookingData::fromBooking($booking);
$form = $this->createForm(BookingType::class, $formData, [
'hx_post' => $this->generateUrl('app_booking_edit', ['id' => $id]),
'hx_target' => '#app',
'hx_swap' => 'innerHTML show:top',
'attr' => ['novalidate' => 'novalidate'],
'travel' => $travelData,
'mutable_fields' => $mutableFields,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->apiClient->updateBooking($formData, false);
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
return new HxRedirectResponse($this->generateUrl('app_booking_edit', ['id' => $id]));
}
return $this->render('booking/edit.html.twig', [
'booking' => $booking,
'travelData' => $travelData,
'mutableFields' => $mutableFields,
'availabilities' => $availabilities,
'form' => $form->createView(),
]);
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly Security $security,
) {
}
#[Route('/bookings', name: 'app_bookings')]
#[IsGranted("ROLE_USER")]
public function index(Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
return $this->render('booking/index.html.twig', [
'bookings' => $bookings->getItems(),
]);
}
}
-160
View File
@@ -1,160 +0,0 @@
<?php
namespace App\Controller;
use App\BusProNet\ApiClient;
use App\BusProNet\DataLoader\PickupDataLoader;
use App\BusProNet\DataLoader\TravelDataLoader;
use App\BusProNet\Model\File;
use App\BusProNet\Model\Notification;
use App\Form\BookingType;
use App\Form\Model\BookingData;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use function Symfony\Component\String\u;
class BookingController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelDataLoader $travelDataLoader,
private readonly PickupDataLoader $pickupDataLoader,
private readonly Security $security,
) {
}
#[Route('/bookings', name: 'app_bookings')]
#[IsGranted("ROLE_USER")]
public function index(Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
return $this->render('booking/index.html.twig', [
'bookings' => $bookings->getItems(),
]);
}
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', requirements: ['id' => '\d+'])]
#[IsGranted("ROLE_USER")]
public function edit(int $id, Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$booking = $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
//$this->denyAccessUnlessGranted('VIEW', $booking);
$travelData = $this->travelDataLoader->loadById($booking->travelId);
if (null === $travelData) {
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
return $this->redirectToRoute('app_bookings');
}
$mutableFields = $this->apiClient->getMutableFields($booking->travelId);
$availabilities = $this->apiClient->getAvailabilities($booking->travelId);
$this->travelDataLoader->enrichServicesData($travelData, $availabilities);
$this->pickupDataLoader->enrichPickupsData($travelData);
$formData = BookingData::fromBooking($booking);
$form = $this->createForm(BookingType::class, $formData, [
'hx_post' => $this->generateUrl('app_booking_edit', ['id' => $id]),
'hx_target' => '#app',
'hx_swap' => 'innerHTML show:top',
'attr' => ['novalidate' => 'novalidate'],
'travel' => $travelData,
'mutable_fields' => $mutableFields,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->apiClient->updateBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $formData);
}
return $this->render('booking/edit.html.twig', [
'booking' => $booking,
'travelData' => $travelData,
'mutableFields' => $mutableFields,
'availabilities' => $availabilities,
'form' => $form->createView(),
]);
}
#[Route('/bookings/{id}/documents', name: 'app_booking_documents', requirements: ['id' => '\d+'])]
#[IsGranted("ROLE_USER")]
public function documents(int $id, Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$file = $this
->apiClient
->getDocuments($bpnUser->getEmail(), $bpnUser->getPassword(), $id, 'Dokumentdruck')
;
if (null === $file || $file instanceof Notification) {
$this->addFlash('error', 'Keine Dokumente vorhanden');
return $this->redirectToRoute('app_bookings');
}
return $this->createDownloadResponse($file);
}
#[Route('/bookings/{id}/confirmation', name: 'app_booking_confirmation', requirements: ['id' => '\d+'])]
#[IsGranted("ROLE_USER")]
public function confirmation(int $id, Request $request): Response
{
$bpnUser = $request->getSession()->get('bpn_user');
if (null === $bpnUser) {
return $this->security->logout();
}
$file = $this
->apiClient
->getDocuments($bpnUser->getEmail(), $bpnUser->getPassword(), $id, 'Vorgangdruck')
;
return $this->createDownloadResponse($file);
}
private function createDownloadResponse(File $file): Response
{
$filename = u($file->filename)->ascii();
$response = new Response($file->content);
$disposition = $response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
$filename,
md5($filename)
);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', $file->mimeType);
return $response;
}
}
+1
View File
@@ -26,6 +26,7 @@ final class BookingType extends AbstractType
->getTransportationServicesByDirection('HIN'), ->getTransportationServicesByDirection('HIN'),
'selectable_transportation_services_fro' => $options['travel'] 'selectable_transportation_services_fro' => $options['travel']
->getTransportationServicesByDirection('RUECK'), ->getTransportationServicesByDirection('RUECK'),
'selectable_pickups' => $options['travel']->pickups,
], ],
'allow_add' => false, 'allow_add' => false,
'allow_delete' => false, 'allow_delete' => false,
+2
View File
@@ -5,8 +5,10 @@ namespace App\Form\Model;
use App\BusProNet\Model\Booking; use App\BusProNet\Model\Booking;
use App\BusProNet\Model\PersonalData; use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use App\Validator\Constraints as AppAssert;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
#[AppAssert\Booking]
class BookingData class BookingData
{ {
public ?Booking $booking = null; public ?Booking $booking = null;
+16 -3
View File
@@ -3,6 +3,7 @@
namespace App\Form\Model; namespace App\Form\Model;
use App\BusProNet\Model\PersonalData; use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -47,9 +48,10 @@ class ParticipantData
public array $rentals = []; public array $rentals = [];
public ?Service $transportationServiceTo = null; public ?Service $transportationServiceTo = null;
public ?Service $transportationServiceFro = null; public ?Service $transportationServiceFro = null;
public ?Pickup $pickup = null;
#[Assert\Callback] #[Assert\Callback]
public function assertBodyMeasurements(ExecutionContextInterface $context): void public function assertBodyMeasurementsValid(ExecutionContextInterface $context): void
{ {
if (0 === count($this->rentals)) { if (0 === count($this->rentals)) {
return; return;
@@ -63,20 +65,31 @@ class ParticipantData
} }
if (empty($this->shoeSize)) { if (empty($this->shoeSize)) {
$context->buildViolation('Bitte angeben Leihmaterial') $context->buildViolation('Bitte angeben wegen Leihmaterial')
->atPath('shoeSize') ->atPath('shoeSize')
->addViolation() ->addViolation()
; ;
} }
if (empty($this->weight)) { if (empty($this->weight)) {
$context->buildViolation('Bitte angeben Leihmaterial') $context->buildViolation('Bitte angeben wegen Leihmaterial')
->atPath('weight') ->atPath('weight')
->addViolation() ->addViolation()
; ;
} }
} }
#[Assert\Callback]
public function assertPickupSelected(ExecutionContextInterface $context): void
{
if (null !== $this->transportationServiceTo && null === $this->pickup) {
$context->buildViolation('Bitte auswählen')
->atPath('pickupTo')
->addViolation()
;
}
}
public static function fromPersonalData(PersonalData $personalData): static public static function fromPersonalData(PersonalData $personalData): static
{ {
$instance = new static(); $instance = new static();
+42 -3
View File
@@ -3,6 +3,7 @@
namespace App\Form; namespace App\Form;
use App\BusProNet\Form\CountryType; use App\BusProNet\Form\CountryType;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use App\Form\Model\ParticipantData; use App\Form\Model\ParticipantData;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@@ -35,6 +36,8 @@ class ParticipantType extends AbstractType
]) ])
->add('gender', ChoiceType::class, [ ->add('gender', ChoiceType::class, [
'label' => 'Geschlecht', 'label' => 'Geschlecht',
'required' => false,
'placeholder' => 'keine Angabe',
'choices' => [ 'choices' => [
'männlich' => 'M', 'männlich' => 'M',
'weiblich' => 'W', 'weiblich' => 'W',
@@ -65,9 +68,9 @@ class ParticipantType extends AbstractType
'required' => false, 'required' => false,
]) ])
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) { ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
/** @var ParticipantData $participant */ /** @var ParticipantData $participantData */
$participant = $event->getData(); $participantData = $event->getData();
$participantIndex = $participant->index; $participantIndex = $participantData->index;
$form = $event->getForm(); $form = $event->getForm();
@@ -139,8 +142,43 @@ class ParticipantType extends AbstractType
'multiple' => false, 'multiple' => false,
'choices' => $options['selectable_transportation_services_fro'], 'choices' => $options['selectable_transportation_services_fro'],
]) ])
->add('pickup', ChoiceType::class, [
'label' => 'Zustieg (bei Busanreise)',
'multiple' => false,
'expanded' => false,
'choices' => $options['selectable_pickups'],
'choice_value' => 'id',
'choice_label' => function (?Pickup $pickup) {
if (null === $pickup) {
return null;
}
$price = $pickup->price;
if (null === $price || 0.0 === $price) {
return $pickup->city;
}
return sprintf('%s (%s€)',
$pickup->city,
number_format($price, 2, ',', '.')
);
},
])
; ;
}) })
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
$data = $event->getData();
$form = $event->getForm();
$transportationId = $data['transportationServiceTo'];
$transportation = $options['travel']->pickups[$transportationId] ?? null;
if (null !== $transportation && 'PKW' === $transportation->subType) {
$form->remove('pickup');
unset($data['pickup']);
}
})
; ;
} }
@@ -155,6 +193,7 @@ class ParticipantType extends AbstractType
'selectable_rentals' => [], 'selectable_rentals' => [],
'selectable_transportation_services_to' => [], 'selectable_transportation_services_to' => [],
'selectable_transportation_services_fro' => [], 'selectable_transportation_services_fro' => [],
'selectable_pickups' => [],
]); ]);
} }
} }
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class Booking extends Constraint
{
public string $message = 'WTF?';
public function getTargets(): array|string
{
return static::CLASS_CONSTRAINT;
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Validator\Constraints;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\Notification;
use App\Form\Model\BookingData;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class BookingValidator extends ConstraintValidator
{
public function __construct(private readonly ApiClient $apiClient)
{
}
public function validate(mixed $value, Constraint $constraint): void
{
/** @var BookingData $booking */
$bookingData = $value;
$result = $this->apiClient->updateBooking($bookingData);
if ($result instanceof Notification) {
$this->context
->buildViolation($result->message)
->addViolation()
;
}
if ($result instanceof BookingUpdate && false === $result->valid) {
$this->context
->buildViolation($constraint->message)
->addViolation()
;
}
}
}
+11
View File
@@ -2,6 +2,14 @@
{% block content %} {% block content %}
{{ form_start(form) }} {{ form_start(form) }}
{% if not form.vars.valid %}
<article>
<header>
<strong>Fehler</strong>
</header>
{{ form_errors(form) }}
</article>
{% endif %}
{% for child in form.participants %} {% for child in form.participants %}
{% set participant = child.vars.data %} {% set participant = child.vars.data %}
<details{% if not child.vars.valid %} open{% endif %}> <details{% if not child.vars.valid %} open{% endif %}>
@@ -57,6 +65,9 @@
<fieldset class="grid"> <fieldset class="grid">
{{ form_row(child.transportationServiceTo) }} {{ form_row(child.transportationServiceTo) }}
{{ form_row(child.transportationServiceFro) }} {{ form_row(child.transportationServiceFro) }}
{% if child.pickup is defined %}
{{ form_row(child.pickup) }}
{% endif %}
</fieldset> </fieldset>
</details> </details>
{% endfor %} {% endfor %}
+12 -22
View File
@@ -1,14 +1,12 @@
{% extends 'layout.html.twig' %} {% extends 'layout.html.twig' %}
{% block content %} {% block content %}
<table> <div class="overflow-auto">
<thead> <table>
<thead>
<tr> <tr>
<th scope="col"> <th scope="col">
Buchungsdatum Buchungsdatum/<br>Reisedatum
</th>
<th scope="col">
Reisedatum
</th> </th>
<th scope="col"> <th scope="col">
Reise Reise
@@ -17,25 +15,19 @@
Vorgangsnr. Vorgangsnr.
</th> </th>
<th scope="col"> <th scope="col">
Preis Preis/<br>offen
</th>
<th scope="col">
offen
</th> </th>
<th scope="col"> <th scope="col">
Status Status
</th> </th>
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for booking in bookings %} {% for booking in bookings %}
<tr> <tr>
<td> <td>
{{ booking.bookingDate | date('d.m.Y') }} {{ booking.bookingDate | date('d.m.Y') }}/<br>{{ booking.travelDate | date('d.m.Y') }}
</td>
<td>
{{ booking.travelDate | date('d.m.Y') }}
</td> </td>
<td> <td>
{{ booking.travel }} {{ booking.travel }}
@@ -44,10 +36,7 @@
{{ booking.bookingNumber }} {{ booking.bookingNumber }}
</td> </td>
<td> <td>
{{ booking.price|format_currency('EUR') }} {{ booking.price|format_currency('EUR') }}/<br>{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
</td>
<td>
{{ booking.balance ? booking.balance|format_currency('EUR') : '-' }}
</td> </td>
<td> <td>
{{ booking.status }} {{ booking.status }}
@@ -76,6 +65,7 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{% endblock %} {% endblock %}
+7
View File
@@ -42,6 +42,13 @@
</nav> </nav>
{% endif %} {% endif %}
<div id="content"> <div id="content">
{% for label, messages in app.flashes %}
{% for message in messages %}
<article>
{{ message }}
</article>
{% endfor %}
{% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
<div id="loading-indicator"> <div id="loading-indicator">
<progress /> <progress />
+19 -17
View File
@@ -5,23 +5,25 @@
Persönliche Daten Persönliche Daten
</h2> </h2>
{{ form_start(form) }} {{ form_start(form) }}
<fieldset class="grid"> <div class="grid">
{{ form_row(form.gender) }} <div>
{{ form_row(form.firstName) }} {{ form_row(form.gender) }}
{{ form_row(form.name) }} {{ form_row(form.firstName) }}
{{ form_row(form.dateOfBirth) }} {{ form_row(form.name) }}
</fieldset> {{ form_row(form.dateOfBirth) }}
<fieldset class="grid"> </div>
{{ form_row(form.street) }} <div>
{{ form_row(form.postCode) }} {{ form_row(form.street) }}
{{ form_row(form.city) }} {{ form_row(form.postCode) }}
{{ form_row(form.country) }} {{ form_row(form.city) }}
</fieldset> {{ form_row(form.country) }}
<fieldset class="grid"> </div>
{{ form_row(form.email) }} <div>
{{ form_row(form.phone) }} {{ form_row(form.email) }}
{{ form_row(form.mobile) }} {{ form_row(form.phone) }}
</fieldset> {{ form_row(form.mobile) }}
</div>
</div>
{{ form_rest(form) }} {{ form_rest(form) }}
<button type="submit"> <button type="submit">
Speichern Speichern