281 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|