Files
myep/src/Controller/Booking/EditController.php
T

281 lines
11 KiB
PHP

<?php
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\DataProcessor\BookingDataProcessor;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\XmlLoader\PickupLoader;
use App\Controller\Traits\BookingDataTrait;
use App\Controller\Traits\HtmxControllerTrait;
use App\Entity\User;
use App\Form\BookingEditType;
use App\Form\Model\BookingDto;
use App\Security\Crypt;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
use App\Service\TravelDataService;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
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;
class EditController extends AbstractController
{
use BookingDataTrait;
use HtmxControllerTrait;
public function __construct(
private readonly ApiClient $apiClient,
private readonly BookingDataProcessor $bookingDataProcessor,
private readonly TravelDataService $travelDataService,
private readonly BookingService $bookingService,
private readonly BookingPriceCalculatorService $priceCalculator,
private readonly PickupLoader $pickupDataLoader,
private readonly CacheInterface $cache,
private readonly Security $security,
private readonly Crypt $crypt,
private readonly LoggerInterface $logger,
) {
}
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', requirements: ['id' => '\d+'])]
#[IsGranted('ROLE_USER')]
public function edit(int $id, Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$email = $user->getEmail();
$password = $this->crypt->decrypt($user->getPassword());
// Fetch original booking data via API and cache result for a short ttl
$bookingData = $this->fetchBookingData($email, $password, $id);
if (null === $bookingData || $bookingData instanceof Notification) {
$this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar');
return $this->redirectToRoute('app_bookings');
}
$this->denyAccessUnlessGranted('EDIT', $bookingData);
// Load according travel data
$travelData = $this->travelDataService->getTravelData($bookingData->dateId);
if (null === $travelData) {
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
return $this->redirectToRoute('app_bookings');
}
// Fetch mutability and availability information via service
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId);
$availabilities = $this->travelDataService->getAvailabilityData($bookingData->dateId);
if (null === $mutableData || null === $availabilities) {
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
return $this->redirectToRoute('app_bookings');
}
// Patch travel data with additional information from above
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
$this->travelDataService->patchMutability($travelData, $mutableData);
// Create DTO for form
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
// Calculate pricing data for template
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($formData);
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($formData);
$participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($formData);
// Group selected rooms for summary display
$availableRooms = $formData->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType(
$formData->getSelectedRooms(),
$availableRooms
);
$form = $this->createForm(BookingEditType::class, $formData, [
'attr' => ['novalidate' => 'novalidate'],
'validation_groups' => ['booking_edit'],
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->logger->info('Initiated booking update', [
'email' => $email,
'booking_id' => $id,
]);
try {
$response = $this->apiClient->updateBooking($formData, true);
if ($response instanceof Notification) {
if (true === $response->isError()) {
$this->addFlash('error', $response->message);
} else {
$this->addFlash('info', $response->message);
}
$this->logger->error('Booking update not successful', [
'email' => $email,
'booking_id' => $id,
'message' => $response->message,
]);
} else {
try {
$cacheKey = sprintf('bpn_booking_%d', $id);
$this->cache->delete($cacheKey);
} catch (InvalidArgumentException $e) {
}
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
$this->logger->info('Booking update successful', [
'email' => $email,
'booking_id' => $id,
]);
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
}
} catch (ApiClientException $e) {
$this->addFlash('error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten');
}
}
return $this->render('booking/edit.html.twig', [
'bookingData' => $bookingData,
'bookingEditDto' => $formData,
'mutableData' => $mutableData,
'form' => $form->createView(),
'pricingData' => $summary['pricing'],
'participantCount' => $summary['participantCount'],
'assignmentCounts' => $roomAssignmentCounts,
'participantPrices' => $participantPrices,
'groupedSelectedRooms' => $groupedSelectedRooms,
]);
}
/**
* HTMX endpoint for refreshing the participant form without validation.
*/
#[Route('/bookings/{id}/edit/refresh', name: 'app_booking_edit_refresh', requirements: ['id' => '\d+'], methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function refreshParticipantForm(int $id, Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$email = $user->getEmail();
$password = $this->crypt->decrypt($user->getPassword());
// Fetch original booking data via API and cache result for a short ttl
$bookingData = $this->fetchBookingData($email, $password, $id);
if (null === $bookingData || $bookingData instanceof Notification) {
return new Response('Buchungsdaten nicht verfügbar', Response::HTTP_BAD_REQUEST);
}
$this->denyAccessUnlessGranted('EDIT', $bookingData);
// Load travel data
$travelData = $this->travelDataService->getTravelData($bookingData->dateId);
if (null === $travelData) {
return new Response('Reisedaten nicht verfügbar', Response::HTTP_BAD_REQUEST);
}
// Fetch mutability and availability information
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId);
$availabilities = $this->travelDataService->getAvailabilityDataCached($bookingData->dateId);
if (null === $mutableData || null === $availabilities) {
return new Response('Reisedaten nicht verfügbar', Response::HTTP_BAD_REQUEST);
}
// Patch travel data
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
$this->travelDataService->patchMutability($travelData, $mutableData);
// Create fresh DTO from booking data
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
// Process form data without validation to capture current state
$form = $this->createForm(BookingEditType::class, $formData, [
'attr' => ['novalidate' => 'novalidate'],
'validation_groups' => false,
]);
$form->handleRequest($request);
// Collect notifications from all participants
$notifications = $this->collectParticipantNotifications($formData);
// Calculate pricing and summary data
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($formData);
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($formData);
$participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($formData);
// Group selected rooms for summary display
$availableRooms = $formData->travel->getAvailableRooms();
$groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType(
$formData->getSelectedRooms(),
$availableRooms
);
// Render updated blocks with fresh data
$response = $this->htmxOobResponse(
'booking/edit.html.twig',
['participants_form', 'booking_summary'],
[
'form' => $form->createView(),
'bookingEditDto' => $formData,
'bookingData' => $bookingData,
'pricingData' => $summary['pricing'],
'participantCount' => $summary['participantCount'],
'assignmentCounts' => $roomAssignmentCounts,
'participantPrices' => $participantPrices,
'groupedSelectedRooms' => $groupedSelectedRooms,
'mutableData' => $mutableData,
]
);
// Add notifications to HTMX trigger header if any exist
if ([] !== $notifications) {
$response->headers->set('HX-Trigger', json_encode([
'showNotifications' => ['notifications' => $notifications],
]));
}
return $response;
}
/**
* Collects all notifications from participants and clears them.
*
* @return array<array{type: string, message: string}> Array of notification messages
*/
private function collectParticipantNotifications(BookingDto $bookingDto): array
{
$notifications = [];
foreach ($bookingDto->participants as $participant) {
if ([] !== $participant->notifications) {
foreach ($participant->notifications as $notification) {
$notifications[] = $notification;
}
// Clear notifications after collecting
$participant->notifications = [];
}
}
return $notifications;
}
}