wip: continue implementation
This commit is contained in:
@@ -17,7 +17,6 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ApiClient
|
||||
@@ -77,6 +76,28 @@ class ApiClient
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function updateNewsletterRegistration(string $email, string $password, PersonalData $personalData): Notification|PersonalData
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Newsletter',
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
'idadresse' => $personalData->addressId,
|
||||
'newsletter' => [
|
||||
'email' => $email,
|
||||
'anmeldung' => $personalData->communication->newsletter ? 'True' : 'False',
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
|
||||
@@ -4,10 +4,11 @@ namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\Availability;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class AvailabilitiesResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
|
||||
@@ -10,10 +10,11 @@ use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Surcharge;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class BookingResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): Booking
|
||||
{
|
||||
@@ -39,7 +40,7 @@ class BookingResponseParser
|
||||
$booking->applicant = $this->parsePersonalData($xml->anmelder);
|
||||
|
||||
$participantsStatus = $this->stringToArray((string) $xml->status_teilnehmer, '/');
|
||||
$booking->participantsStatus = $this->arrayToOneBased($participantsStatus);
|
||||
$booking->participantsStatus = array_combine(range(1, count($participantsStatus)), $participantsStatus);
|
||||
|
||||
$booking->participants = $this->parseParticipants($xml);
|
||||
|
||||
@@ -120,7 +121,7 @@ class BookingResponseParser
|
||||
return $personalData;
|
||||
}
|
||||
|
||||
private function parseServices(\SimpleXMLElement $xml, string $type): array
|
||||
private function parseServices(\SimpleXMLElement $xml, string $category): array
|
||||
{
|
||||
$services = [];
|
||||
|
||||
@@ -130,6 +131,8 @@ class BookingResponseParser
|
||||
|
||||
$service = new Service();
|
||||
$service->id = $id;
|
||||
$service->category = $category;
|
||||
$service->source = Service::SOURCE_BOOKING;
|
||||
$service->label = (string) $attributes['leistung'];
|
||||
$service->dateFrom = $attributes['termin'] ? $this->stringToDate((string) $attributes['termin']) : null;
|
||||
$service->dateTo = $attributes['terminbis'] ? $this->stringToDate((string) $attributes['terminbis']) : null;
|
||||
@@ -144,7 +147,7 @@ class BookingResponseParser
|
||||
}, $this->stringToArray((string) $attributes['einzelpreis'], '/')
|
||||
);
|
||||
$service->individualPrice = array_combine($mapping, $individualPrices);
|
||||
if (Service::CATEGORY_TRANSPORTATION === $type) {
|
||||
if (Service::CATEGORY_TRANSPORTATION === $category) {
|
||||
$service->direction = (string) $attributes['richtung'];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\BookingUpdate;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class BookingUpdateResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BookingUpdate
|
||||
{
|
||||
|
||||
@@ -4,10 +4,11 @@ namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class BookingsResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
@@ -23,6 +24,7 @@ class BookingsResponseParser
|
||||
$booking->price = $this->stringToFloat((string) $item->preis);
|
||||
$booking->bookingDate = $this->stringToDateTime((string) $item->buchungsdatum);
|
||||
$booking->travel = (string) $item->reise;
|
||||
$booking->travelId = (int) $item->idreise;
|
||||
$booking->travelDate = $this->stringToDate((string) $item->reisedatum);
|
||||
$booking->document = $this->stringToBool((string) $item->reisedokument);
|
||||
$booking->payment = $this->stringToFloat((string) $item->zahlung);
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\CrmSelection;
|
||||
use App\BusProNet\Model\CrmSelectionGroup;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class CrmAttributesResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
private const BPN_CRM_ID_ADMIN = 1292;
|
||||
private const BPN_CRM_ID_MANAGER = 1293;
|
||||
|
||||
@@ -4,10 +4,11 @@ namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\MutableData;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class MutableDataResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
|
||||
@@ -5,9 +5,12 @@ namespace App\BusProNet\ApiResponseParser;
|
||||
use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
|
||||
class PersonalDataResponseParser
|
||||
{
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): PersonalData
|
||||
{
|
||||
$addressXml = $xml->adressdaten;
|
||||
@@ -30,24 +33,25 @@ class PersonalDataResponseParser
|
||||
$personalData->weight = $xml->sonstiges2 ? (int) $xml->sonstiges2 : null;
|
||||
$personalData->shoeSize = $xml->sonstiges3 ? (int) $xml->sonstiges3 : null;
|
||||
|
||||
$postalXml = $addressXml->anschrift;
|
||||
if ($postalXml = $addressXml->anschrift) {
|
||||
$address = new Address();
|
||||
$address->street = (string) $postalXml->strasse;
|
||||
$address->postCode = (string) $postalXml->plz;
|
||||
$address->city = (string) $postalXml->ort;
|
||||
$address->country = (string) $postalXml->land;
|
||||
|
||||
$address = new Address();
|
||||
$address->street = (string) $postalXml->strasse;
|
||||
$address->postCode = (string) $postalXml->plz;
|
||||
$address->city = (string) $postalXml->ort;
|
||||
$address->country = (string) $postalXml->land;
|
||||
$personalData->address = $address;
|
||||
}
|
||||
|
||||
$personalData->address = $address;
|
||||
if ($contactXml = $addressXml->kommunikation) {
|
||||
$communication = new Communication();
|
||||
$communication->phone = (string) $contactXml->telefonprivat;
|
||||
$communication->mobile = (string) $contactXml->telefonmobil;
|
||||
$communication->email = (string) $contactXml->email;
|
||||
$communication->newsletter = $this->stringToBool((string) $contactXml->newsletter);
|
||||
|
||||
$contactXml = $addressXml->kommunikation;
|
||||
|
||||
$communication = new Communication();
|
||||
$communication->phone = (string) $contactXml->telefonprivat;
|
||||
$communication->mobile = (string) $contactXml->telefonmobil;
|
||||
$communication->email = (string) $contactXml->email;
|
||||
|
||||
$personalData->communication = $communication;
|
||||
$personalData->communication = $communication;
|
||||
}
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ResponseParser
|
||||
switch ($subType) {
|
||||
case 'Adressdaten':
|
||||
case 'Adressdaten_Ändern':
|
||||
case 'Newsletter':
|
||||
return (new PersonalDataResponseParser())->parse($xml);
|
||||
case 'SelektionCRM':
|
||||
case 'SelektionCRM_Ändern':
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\ApiResponseParser\ResponseParserTrait;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
abstract class AbstractDataLoader
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
use TypeConversionTrait;
|
||||
|
||||
public function __construct(protected readonly CacheInterface $cache, protected readonly ?string $xmlPath = null)
|
||||
{
|
||||
|
||||
@@ -11,38 +11,90 @@ use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class TravelDataLoader extends AbstractDataLoader
|
||||
{
|
||||
public function __construct(
|
||||
private readonly HotelDataLoader $hotelDataLoader,
|
||||
CacheInterface $cache,
|
||||
?string $xmlPath = null
|
||||
) {
|
||||
parent::__construct($cache, $xmlPath);
|
||||
}
|
||||
|
||||
public function generateMapping(): array
|
||||
{
|
||||
return $this->cache->get('bpn_travels_mapping', function (ItemInterface $item) {
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($this->xmlPath)->name('Ziel_*.xml');
|
||||
|
||||
$mapping = [];
|
||||
$hotels = $this->hotelDataLoader->loadAll();
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$xml = simplexml_load_file($file->getRealPath());
|
||||
|
||||
foreach ($xml->reise->termin as $travelXml) {
|
||||
$attributes = $travelXml->attributes();
|
||||
|
||||
$travelId = (int) $attributes['idbuspro'];
|
||||
|
||||
$mapping[$travelId] = [
|
||||
'id' => (int) $attributes['idbuspro'],
|
||||
'label' => (string) $travelXml->text,
|
||||
'dateFrom' => $this->stringToDate((string) $attributes['termin']),
|
||||
'dateTo' => $this->stringToDate((string) $attributes['bis']),
|
||||
'hotels' => [],
|
||||
'file' => $file->getRealPath(),
|
||||
];
|
||||
|
||||
foreach ($travelXml->hotel as $hotelXml) {
|
||||
$hotelId = (int) $hotelXml->attributes()['idbuspro'];
|
||||
|
||||
$mapping[$travelId]['hotels'][$hotelId] = [
|
||||
'id' => $hotelId,
|
||||
'label' => $hotels[$hotelId]->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
});
|
||||
}
|
||||
|
||||
public function loadById(int $id, ?string $filename = null): ?Travel
|
||||
{
|
||||
if (null !== $filename) {
|
||||
return $this->loadXml($id, $filename);
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($this->xmlPath)->name('Ziel_*.xml');
|
||||
$mapping = $this->generateMapping();
|
||||
|
||||
foreach ($finder as $file) {
|
||||
if (null !== $xml = $this->loadXml($id, $file->getRealPath())) {
|
||||
return $xml;
|
||||
}
|
||||
if (false === isset($mapping[$id])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
$file = $mapping[$id]['file'];
|
||||
|
||||
return $this->loadXml($id, $file);
|
||||
}
|
||||
|
||||
private function loadXml(int $id, string $filename): ?Travel
|
||||
{
|
||||
$xml = simplexml_load_file($filename);
|
||||
|
||||
foreach ($xml->reise->termin as $travel) {
|
||||
if ($id === (int) $travel->attributes()['idbuspro']) {
|
||||
return $this->parseXml($travel);
|
||||
}
|
||||
$travelXml = $xml->xpath(sprintf('//reise/termin[@idbuspro="%d"]', $id));
|
||||
|
||||
if (0 === count($travelXml)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->parseXml($travelXml[0]);
|
||||
}
|
||||
|
||||
public function parseXml(\SimpleXMLElement $xml): Travel
|
||||
@@ -100,6 +152,7 @@ class TravelDataLoader extends AbstractDataLoader
|
||||
$serviceId = (int) $attributes['idbuspro'];
|
||||
|
||||
$service = new Service();
|
||||
$service->source = Service::SOURCE_TRAVEL;
|
||||
$service->category = Service::CATEGORY_ADDITIONAL;
|
||||
$service->id = $serviceId;
|
||||
$service->subType = (string) $attributes['unterart'];
|
||||
@@ -125,6 +178,7 @@ class TravelDataLoader extends AbstractDataLoader
|
||||
$serviceId = (int) $attributes['idbuspro'];
|
||||
|
||||
$service = new Service();
|
||||
$service->source = Service::SOURCE_TRAVEL;
|
||||
$service->category = Service::CATEGORY_TRANSPORTATION;
|
||||
$service->id = $serviceId;
|
||||
$service->subType = (string) $attributes['unterart'];
|
||||
@@ -223,4 +277,15 @@ class TravelDataLoader extends AbstractDataLoader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function patchBookings(BaseData $bookings): void
|
||||
{
|
||||
foreach ($bookings->getItems() as $booking) {
|
||||
if (null === $travelId = $booking->travelId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking->travelData = $this->loadById($travelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class Booking
|
||||
public ?int $travelId = null;
|
||||
public ?string $travelCode = null;
|
||||
public ?\DateTimeImmutable $travelDate = null;
|
||||
public ?Travel $travelData = null;
|
||||
public ?int $hotelId = null;
|
||||
public ?string $hotelName = null;
|
||||
public ?bool $document = null;
|
||||
|
||||
@@ -7,6 +7,7 @@ class Communication
|
||||
public ?string $phone = null;
|
||||
public ?string $mobile = null;
|
||||
public ?string $email = null;
|
||||
public bool $newsletter = false;
|
||||
|
||||
public function toPayload(): array
|
||||
{
|
||||
@@ -14,6 +15,7 @@ class Communication
|
||||
'email' => $this->email,
|
||||
'telefonmobil' => $this->mobile,
|
||||
'telefonprivat' => $this->phone,
|
||||
'newsletter' => $this->newsletter ? 'True' : 'False',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,13 @@ class PersonalData
|
||||
{
|
||||
public ?int $addressId = null;
|
||||
public ?int $personId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $name = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $firstName = null;
|
||||
|
||||
public ?string $salutation = null;
|
||||
public ?string $title = null;
|
||||
public ?string $gender = null;
|
||||
@@ -20,8 +23,10 @@ class PersonalData
|
||||
public ?string $shoeSize = null;
|
||||
public ?string $weight = null;
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
|
||||
#[Assert\Valid()]
|
||||
public ?Address $address = null;
|
||||
|
||||
#[Assert\Valid()]
|
||||
public ?Communication $communication = null;
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ namespace App\BusProNet\Model;
|
||||
|
||||
class Service
|
||||
{
|
||||
public const SOURCE_TRAVEL = 'travel';
|
||||
public const SOURCE_BOOKING = 'booking';
|
||||
|
||||
public const CATEGORY_ADDITIONAL = 'additional';
|
||||
public const CATEGORY_TRANSPORTATION = 'transportation';
|
||||
|
||||
@@ -14,6 +17,7 @@ class Service
|
||||
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
|
||||
|
||||
public ?int $id = null;
|
||||
public ?string $source = null;
|
||||
public ?string $category = null;
|
||||
public ?string $status = null;
|
||||
public bool $mandatory = false;
|
||||
|
||||
+12
-9
@@ -1,19 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
namespace App\BusProNet;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
|
||||
trait ResponseParserTrait
|
||||
trait TypeConversionTrait
|
||||
{
|
||||
private function arrayToOneBased(array $data): array
|
||||
{
|
||||
$keys = range(1, count($data));
|
||||
|
||||
return array_combine($keys, $data);
|
||||
}
|
||||
|
||||
protected function stringToArray(string $string, string $separator = ','): array
|
||||
{
|
||||
return explode($separator, $string);
|
||||
@@ -24,11 +17,21 @@ trait ResponseParserTrait
|
||||
return (float) str_replace(['.', ','], ['', '.'], $string);
|
||||
}
|
||||
|
||||
protected function floatToString(float $float): string
|
||||
{
|
||||
return number_format($float, 2, ',', '');
|
||||
}
|
||||
|
||||
protected function stringToBool(string $string): bool
|
||||
{
|
||||
return 'True' === $string;
|
||||
}
|
||||
|
||||
protected function boolToString(bool $bool): string
|
||||
{
|
||||
return $bool ? 'True' : 'False';
|
||||
}
|
||||
|
||||
protected function stringToDate(string $string): ?\DateTimeImmutable
|
||||
{
|
||||
try {
|
||||
@@ -7,13 +7,14 @@ 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;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class EditController extends AbstractController
|
||||
{
|
||||
@@ -21,6 +22,7 @@ class EditController extends AbstractController
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly TravelDataLoader $travelDataLoader,
|
||||
private readonly PickupDataLoader $pickupDataLoader,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
@@ -35,8 +37,14 @@ class EditController extends AbstractController
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
// Fetch original bookingData data via API
|
||||
$bookingData = $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
|
||||
// Fetch original bookingData data via API and cache result for a short ttl
|
||||
$cacheKeyList = sprintf('bpn_bookings_%s', sha1($bpnUser->getUserIdentifier()));
|
||||
$cacheKeySingle = sprintf('bpn_booking_%d', $id);
|
||||
$bookingData = $this->cache->get($cacheKeySingle, function (ItemInterface $item) use ($bpnUser, $id) {
|
||||
$item->expiresAfter(300);
|
||||
|
||||
return $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
|
||||
});
|
||||
|
||||
if (null === $bookingData) {
|
||||
$this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar');
|
||||
@@ -68,10 +76,9 @@ class EditController extends AbstractController
|
||||
$formData = BookingData::fromBooking($bookingData, $travelData);
|
||||
|
||||
$form = $this->createForm(BookingType::class, $formData, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'hx_post' => $this->generateUrl('app_booking_edit', ['id' => $id]),
|
||||
'hx_target' => '#app',
|
||||
'hx_swap' => 'innerHTML show:top',
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
@@ -79,9 +86,12 @@ class EditController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->apiClient->updateBooking($formData, false);
|
||||
|
||||
$this->cache->delete($cacheKeySingle);
|
||||
$this->cache->delete($cacheKeyList);
|
||||
|
||||
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_booking_edit', ['id' => $id]));
|
||||
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
|
||||
}
|
||||
|
||||
return $this->render('booking/edit.html.twig', [
|
||||
|
||||
@@ -3,17 +3,22 @@
|
||||
namespace App\Controller\Booking;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\DataLoader\TravelDataLoader;
|
||||
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;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly TravelDataLoader $travelDataLoader,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
@@ -28,7 +33,16 @@ class IndexController extends AbstractController
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
|
||||
// Cache bookings for a short ttl
|
||||
$cacheKey = sprintf('bpn_bookings_%s', sha1($bpnUser->getUserIdentifier()));
|
||||
$bookings = $this->cache->get($cacheKey, function (ItemInterface $item) use ($bpnUser) {
|
||||
$item->expiresAfter(300);
|
||||
|
||||
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
|
||||
$this->travelDataLoader->patchBookings($bookings);
|
||||
|
||||
return $bookings;
|
||||
});
|
||||
|
||||
return $this->render('booking/index.html.twig', [
|
||||
'bookings' => $bookings->getItems(),
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_index')]
|
||||
#[IsGranted('PUBLIC_ACCESS')]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('index/index.html.twig');
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class PersonalDataController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->apiClient->updatePersonalData($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
|
||||
$this->addFlash('success', 'Personal data updated.');
|
||||
$this->addFlash('success', 'Deine persönlichen Daten wurden aktualisiert');
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', $e->getMessage());
|
||||
}
|
||||
@@ -55,4 +55,31 @@ class PersonalDataController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/personal-data/newsletter', name: 'app_personal_data_newsletter', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function newsletter(Request $request): Response
|
||||
{
|
||||
$bpnUser = $request->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$personalData = $this
|
||||
->apiClient
|
||||
->getPersonalData($bpnUser->getEmail(), $bpnUser->getPassword())
|
||||
;
|
||||
|
||||
$personalData->communication->newsletter = !$personalData->communication->newsletter;
|
||||
|
||||
try {
|
||||
$this->apiClient->updateNewsletterRegistration($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
|
||||
$this->addFlash('success', 'Deine Anmeldung zum Newsletter wurde aktualisiert');
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_login')]
|
||||
#[Route('/login', name: 'app_login')]
|
||||
#[IsGranted('PUBLIC_ACCESS')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
#[AsEventListener(event: LogoutEvent::class, method: 'onLogout')]
|
||||
class LogoutListener
|
||||
{
|
||||
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
public function onLogout(LogoutEvent $event): void
|
||||
{
|
||||
$event->setResponse(new HxRedirectResponse($this->urlGenerator->generate('app_index')));
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,11 @@ final class BookingType extends AbstractType
|
||||
$form->add('participants', CollectionType::class, [
|
||||
'entry_type' => ParticipantType::class,
|
||||
'entry_options' => [
|
||||
'selectable_courses' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_COURSES),
|
||||
'selectable_ski_passes' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_SKI_PASS),
|
||||
'selectable_services' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_ADDITIONAL),
|
||||
'selectable_board' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_BOARD),
|
||||
'selectable_rentals' => $travelData->getAdditionalServicesByGroup(Service::TOKEN_RENTALS),
|
||||
'selectable_courses' => $this->mergeSelectableServices($data, Service::TOKEN_COURSES),
|
||||
'selectable_ski_passes' => $this->mergeSelectableServices($data, Service::TOKEN_SKI_PASS),
|
||||
'selectable_services' => $this->mergeSelectableServices($data, Service::TOKEN_ADDITIONAL),
|
||||
'selectable_board' => $this->mergeSelectableServices($data, Service::TOKEN_BOARD),
|
||||
'selectable_rentals' => $this->mergeSelectableServices($data, Service::TOKEN_RENTALS),
|
||||
'selectable_transportation_services_to' => $travelData
|
||||
->getTransportationServicesByDirection('HIN'),
|
||||
'selectable_transportation_services_fro' => $travelData
|
||||
@@ -46,10 +46,28 @@ final class BookingType extends AbstractType
|
||||
});
|
||||
}
|
||||
|
||||
private function mergeSelectableServices(BookingData $data, mixed $subType): array
|
||||
{
|
||||
// combine selectable services from travel data with additional services
|
||||
// from booking data
|
||||
$selectableServices = $data->travel->getAdditionalServicesByGroup($subType);
|
||||
$selectableServiceIds = array_map(function (Service $service) {
|
||||
return $service->id;
|
||||
}, $selectableServices);
|
||||
|
||||
foreach ($data->booking->getAdditionalServicesByGroup($subType) as $item) {
|
||||
if (false === in_array($item->id, $selectableServiceIds)) {
|
||||
$selectableServices[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $selectableServices;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults(['data_class' => BookingData::class])
|
||||
;
|
||||
$resolver->setDefaults([
|
||||
'data_class' => BookingData::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+173
-125
@@ -51,7 +51,7 @@ class ParticipantType extends AbstractType
|
||||
'preferred_choices' => ['D', 'A', 'CH'],
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Email',
|
||||
'label' => 'E-Mail',
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
@@ -71,142 +71,190 @@ class ParticipantType extends AbstractType
|
||||
;
|
||||
}
|
||||
|
||||
$builder
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
|
||||
/** @var ParticipantData $participantData */
|
||||
$participantData = $event->getData();
|
||||
$participantIndex = $participantData->index;
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
|
||||
/** @var ParticipantData $participantData */
|
||||
$participantData = $event->getData();
|
||||
$participantIndex = $participantData->index;
|
||||
|
||||
$form = $event->getForm();
|
||||
$form = $event->getForm();
|
||||
|
||||
$commonChoiceFieldOptions = [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => function (?Service $service) use ($participantIndex) {
|
||||
if (null === $service) {
|
||||
return null;
|
||||
}
|
||||
$price = $service->individualPrice[$participantIndex] ?? $service->price;
|
||||
if (null === $price || 0.0 === $price) {
|
||||
return $service->label;
|
||||
}
|
||||
$commonChoiceFieldOptions = [
|
||||
'required' => false,
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => function (?Service $service) use ($participantIndex) {
|
||||
if (null === $service) {
|
||||
return null;
|
||||
}
|
||||
$price = $service->individualPrice[$participantIndex] ?? $service->price;
|
||||
if (null === $price || 0.0 === $price) {
|
||||
return $service->label;
|
||||
}
|
||||
|
||||
return sprintf('%s (%s€)',
|
||||
$service->label,
|
||||
number_format($price, 2, ',', '.')
|
||||
);
|
||||
},
|
||||
'choice_attr' => function (?Service $service) {
|
||||
if (true === $service->mandatory) {
|
||||
return sprintf('%s (%s €)',
|
||||
$service->label,
|
||||
number_format($price, 2, ',', '.')
|
||||
);
|
||||
},
|
||||
'choice_attr' => function (?Service $service) {
|
||||
$attributes = [
|
||||
'data-available' => $service->available,
|
||||
'data-availabilities-target' => 'field',
|
||||
];
|
||||
|
||||
// forcibly select mandatory services
|
||||
if (true === $service->mandatory) {
|
||||
$attributes['checked'] = true;
|
||||
$attributes['disabled'] = true;
|
||||
}
|
||||
|
||||
// disable selection of services only available in booking data
|
||||
if (Service::SOURCE_BOOKING === $service->source) {
|
||||
$attributes['disabled'] = true;
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
},
|
||||
'choice_filter' => function (Service $service) use ($participantData) {
|
||||
// remove choices only available in booking data and
|
||||
// which are not mapped to the current participant
|
||||
if (
|
||||
Service::SOURCE_TRAVEL === $service->source ||
|
||||
Service::CATEGORY_ADDITIONAL !== $service->category
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($participantData->index, $service->mapping);
|
||||
},
|
||||
];
|
||||
|
||||
// Additional services
|
||||
if (true === $options['additional_services_mutable']) {
|
||||
if (0 < count($options['selectable_courses'])) {
|
||||
$form->add('courses', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Kurse',
|
||||
'choices' => $options['selectable_courses'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_services'])) {
|
||||
$form->add('additionalServices', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Zusatzleistungen',
|
||||
'choices' => $options['selectable_services'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_ski_passes'])) {
|
||||
$form->add('skiPass', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Skipass',
|
||||
'choices' => $options['selectable_ski_passes'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_board'])) {
|
||||
$form->add('board', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Verpflegung',
|
||||
'choices' => $options['selectable_board'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_rentals'])) {
|
||||
$form->add('rentals', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Verleih',
|
||||
'choices' => $options['selectable_rentals'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Transportation
|
||||
if (true === $options['transportation_services_mutable']) {
|
||||
$form
|
||||
->add('transportationServiceTo', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Anreise',
|
||||
'multiple' => false,
|
||||
'choices' => $options['selectable_transportation_services_to'],
|
||||
'choice_attr' => function (?Service $service) {
|
||||
return [
|
||||
'checked' => true,
|
||||
'disabled' => true,
|
||||
'data-available' => $service->available,
|
||||
'data-availabilities-target' => 'field',
|
||||
'data-action' => 'select-toggle#toggle',
|
||||
'data-select-toggle-value' => $service->subType,
|
||||
];
|
||||
}
|
||||
},
|
||||
])
|
||||
->add('transportationServiceFro', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Rückreise',
|
||||
'multiple' => false,
|
||||
'choices' => $options['selectable_transportation_services_fro'],
|
||||
]);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
];
|
||||
// Pickup
|
||||
if (true === $options['pickups_mutable']) {
|
||||
$form
|
||||
->add('pickup', ChoiceType::class, [
|
||||
'label' => 'Zustieg',
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
'choices' => $options['selectable_pickups'],
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => function (?Pickup $pickup) {
|
||||
if (null === $pickup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Additional services
|
||||
if (true === $options['additional_services_mutable']) {
|
||||
$price = $pickup->price;
|
||||
|
||||
if (0 < count($options['selectable_courses'])) {
|
||||
$form->add('courses', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Kurse',
|
||||
'choices' => $options['selectable_courses'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_services'])) {
|
||||
$form->add('additionalServices', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Zusatzleistungen',
|
||||
'choices' => $options['selectable_services'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_ski_passes'])) {
|
||||
$form->add('skiPass', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Skipass',
|
||||
'choices' => $options['selectable_ski_passes'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_board'])) {
|
||||
$form->add('board', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Verpflegung',
|
||||
'choices' => $options['selectable_board'],
|
||||
]);
|
||||
}
|
||||
if (0 < count($options['selectable_rentals'])) {
|
||||
$form->add('rentals', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Verleih',
|
||||
'choices' => $options['selectable_rentals'],
|
||||
]);
|
||||
}
|
||||
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']);
|
||||
}
|
||||
|
||||
// forcibly select mandatory services that potentially have been disabled in PRE_SET_DATA
|
||||
$mandatoryServices = array_filter($options['selectable_services'], function (Service $service) use ($data) {
|
||||
return true === $service->mandatory ||
|
||||
(Service::SOURCE_BOOKING === $service->source && in_array($data['index'], $service->mapping));
|
||||
});
|
||||
|
||||
if (0 < count($mandatoryServices)) {
|
||||
if (false === isset($data['additionalServices'])) {
|
||||
$data['additionalServices'] = [];
|
||||
}
|
||||
|
||||
// Transportation
|
||||
if (true === $options['transportation_services_mutable']) {
|
||||
$form
|
||||
->add('transportationServiceTo', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Anreise',
|
||||
'multiple' => false,
|
||||
'choices' => $options['selectable_transportation_services_to'],
|
||||
])
|
||||
->add('transportationServiceFro', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Rückreise',
|
||||
'multiple' => false,
|
||||
'choices' => $options['selectable_transportation_services_fro'],
|
||||
]);
|
||||
}
|
||||
$mandatoryServiceIds = array_map(function (Service $service) {
|
||||
return $service->id;
|
||||
}, $mandatoryServices);
|
||||
|
||||
// Pickup
|
||||
if (true === $options['pickups_mutable']) {
|
||||
$form
|
||||
->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;
|
||||
}
|
||||
$allServiceIds = [...$mandatoryServiceIds, ...$data['additionalServices']];
|
||||
$data['additionalServices'] = array_unique($allServiceIds);
|
||||
}
|
||||
|
||||
$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']);
|
||||
}
|
||||
})
|
||||
;
|
||||
$event->setData($data);
|
||||
});
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
||||
@@ -18,9 +18,9 @@ class PersonalDataType extends AbstractType
|
||||
->add('gender', ChoiceType::class, [
|
||||
'label' => 'Gender',
|
||||
'choices' => [
|
||||
'M' => 'M',
|
||||
'W' => 'W',
|
||||
'D' => 'D',
|
||||
'männlich' => 'M',
|
||||
'weiblich' => 'W',
|
||||
'divers' => 'D',
|
||||
],
|
||||
])
|
||||
->add('firstName', TextType::class, [
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Menu;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
|
||||
class MenuBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FactoryInterface $factory,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createMainMenu(): ItemInterface
|
||||
{
|
||||
$menu = $this->factory->createItem('root', [
|
||||
'childrenAttributes' => [
|
||||
'class' => 'menu menu--main',
|
||||
],
|
||||
]);
|
||||
|
||||
$menu->addChild('Meine Daten', [
|
||||
'route' => 'app_personal_data',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Persönliche Daten',
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Meine Buchungen', [
|
||||
'route' => 'app_bookings',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Meine Buchungen',
|
||||
],
|
||||
'extras' => [
|
||||
'routes' => [
|
||||
'app_booking_edit',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Logout', [
|
||||
'route' => 'app_logout',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Logout',
|
||||
],
|
||||
]);
|
||||
|
||||
return $menu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class AppExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('file_size', [AppRuntime::class, 'formatBytes']),
|
||||
new TwigFilter('file_icon', [AppRuntime::class, 'fileIconFilter'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('format_money', [AppRuntime::class, 'formatMoney']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('icon', [AppRuntime::class, 'renderIcon'], ['needs_environment' => true, 'is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
use Twig\Extra\Intl\IntlExtension;
|
||||
|
||||
class AppRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(private readonly IntlExtension $intlExtension)
|
||||
{
|
||||
}
|
||||
|
||||
public function fileIconFilter(Environment $environment, string $mimeType, ?string $classes = 'w-4 h-4'): string
|
||||
{
|
||||
$icon = match ($mimeType) {
|
||||
'application/pdf' => 'pdf',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'excel',
|
||||
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'word',
|
||||
default => 'download',
|
||||
};
|
||||
|
||||
return $this->renderIcon($environment, $icon, $classes);
|
||||
}
|
||||
|
||||
public function formatBytes(int $bytes, ?int $precision = 2): string
|
||||
{
|
||||
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
|
||||
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)) . @$size[$factor];
|
||||
}
|
||||
|
||||
public function formatMoney(int $amount): string
|
||||
{
|
||||
// Amounts are stored as integers so divide by 100 first
|
||||
$amount = $amount / 100;
|
||||
|
||||
return $this->intlExtension->formatCurrency($amount, 'EUR');
|
||||
}
|
||||
|
||||
public function renderIcon(Environment $environment, string $icon, string $classes = 'w-5 h-5'): string
|
||||
{
|
||||
return $environment->render('_partials/_icon.html.twig', [
|
||||
'icon' => $icon,
|
||||
'class' => $classes,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user