feat: duplicate an accommodation booking into a new draft
addresses #869f6z44f
This commit is contained in:
1 parent
2aa98b81fe
commit
29f295abf6
9 files changed
+470
No files matched your search
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin\AccommodationBooking;
|
||||
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Service\AccommodationBookingService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Copying is its own named action rather than a flag on the edit route the way it is for the
|
||||
* catalog entities (see Admin\AdditionalService\EditController): a booking carries a lifecycle,
|
||||
* so the copy has to be reset and written before anybody edits it, and the edit form is built
|
||||
* from a persisted record's house and service catalog.
|
||||
*
|
||||
* Unlike absagen and löschen this has no status guard — the original is not touched, so a
|
||||
* confirmed or discarded booking may be copied just as well as a draft.
|
||||
*/
|
||||
#[IsGranted('ROLE_GROUPS_MANAGER')]
|
||||
class DuplicateController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccommodationBookingService $bookingService,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/accommodation-booking/{id}/duplicate', name: 'app_admin_accommodationbooking_duplicate')]
|
||||
public function index(AccommodationBooking $booking, Request $request): Response
|
||||
{
|
||||
if ($request->isMethod(Request::METHOD_POST)) {
|
||||
if (!$this->isCsrfTokenValid('duplicate_accommodation_booking_'.$booking->getId(), $request->request->getString('_token'))) {
|
||||
throw $this->createAccessDeniedException('Invalid CSRF token.');
|
||||
}
|
||||
|
||||
$copy = $this->bookingService->duplicate($booking);
|
||||
|
||||
$this->addFlash('success', 'Die Kopie wurde als neuer Entwurf angelegt.');
|
||||
|
||||
$this->logger->info('Duplicated accommodation booking', [
|
||||
'id' => $booking->getId(),
|
||||
'copyId' => $copy->getId(),
|
||||
'groupName' => $copy->getGroupName(),
|
||||
]);
|
||||
|
||||
// Straight into the copy's edit form — the copy exists to be changed, and that is
|
||||
// the only reason it was made. The return url is forwarded still encoded, the way
|
||||
// return_url() handed it over, so "zurück" still leads to the list this started from.
|
||||
$parameters = ['id' => $copy->getId()];
|
||||
$returnUrl = $request->query->getString('r');
|
||||
if ('' !== $returnUrl) {
|
||||
$parameters['r'] = $returnUrl;
|
||||
}
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_admin_accommodationbooking_edit', $parameters));
|
||||
}
|
||||
|
||||
return $this->render('admin/accommodation_booking/modal_duplicate.html.twig', [
|
||||
'booking' => $booking,
|
||||
'csrf_token_id' => 'duplicate_accommodation_booking_'.$booking->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,34 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
}
|
||||
|
||||
/**
|
||||
* A copy is a new record, not a second view of the original: it gets its own identity and
|
||||
* its own access link token — sharing that token would put two records behind one customer
|
||||
* link — and every mark the lifecycle left is wiped, so the copy starts as an Entwurf that
|
||||
* has never been in front of a customer. The origin is rewritten too: however the original
|
||||
* came about, the copy comes about as an offer the office prepared, which is what keeps the
|
||||
* field truthful.
|
||||
*
|
||||
* The price snapshot, the service snapshots and all four discounts are deliberately carried
|
||||
* over untouched. A copy is made to vary an offer that was already quoted, so it has to show
|
||||
* the same numbers as the record it came from rather than a re-quote at today's catalog
|
||||
* prices; saving it through the edit form recomputes it.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->status = AccommodationBookingStatus::Draft;
|
||||
$this->origin = AccommodationBookingOrigin::Offer;
|
||||
$this->accessLinkIssuedAt = null;
|
||||
$this->acceptedAt = null;
|
||||
$this->confirmedAt = null;
|
||||
$this->createdAt = null;
|
||||
$this->updatedAt = null;
|
||||
$this->createdBy = null;
|
||||
$this->updatedBy = null;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
||||
@@ -576,6 +576,33 @@ class AccommodationBookingService
|
||||
* @throws ImplausibleBookingPriceException if the new house prices the stay implausibly high;
|
||||
* the booking is left at its old house
|
||||
*/
|
||||
/**
|
||||
* Copies a booking into a fresh Entwurf, so the office can build a variation of an offer
|
||||
* instead of keying the whole thing in again. Everything the customer was quoted comes
|
||||
* along verbatim — house, dates, pax, the board and additional service snapshots, the
|
||||
* discounts and the stored price breakdown — while AccommodationBooking::__clone() resets
|
||||
* the identity and the lifecycle.
|
||||
*
|
||||
* Allowed from any status, including Bestätigt and Absage: nothing on the original is
|
||||
* touched, and "same group, next season" is exactly the case this exists for. The price is
|
||||
* deliberately not recomputed, see __clone().
|
||||
*/
|
||||
public function duplicate(AccommodationBooking $booking): AccommodationBooking
|
||||
{
|
||||
$copy = clone $booking;
|
||||
|
||||
// Marks the copy where the office looks for it — the list is sorted and scanned by
|
||||
// group name, and two identical rows are otherwise impossible to tell apart.
|
||||
if (null !== $copy->getGroupName()) {
|
||||
$copy->setGroupName($copy->getGroupName().' (Kopie)');
|
||||
}
|
||||
|
||||
$this->entityManager->persist($copy);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
public function changeAccommodation(AccommodationBooking $booking, Accommodation $accommodation): void
|
||||
{
|
||||
if (!$booking->isDraft() || $booking->getAccommodation() === $accommodation) {
|
||||
|
||||
Reference in new issue
Block a user