wip: implement admin crud

This commit is contained in:
Björn Fromme committed 2026-07-16 09:03:42 +02:00
1 parent 306cd6bec4
commit e902a5e298
78 files changed
+2555 -101

No files matched your search

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Groups\Accommodation;
use App\Form\Admin\Groups\AccommodationType;
use App\Htmx\HxRedirectResponse;
use App\Repository\Groups\AccommodationRepository;
use App\Service\CmsDataProvider;
use Doctrine\ORM\EntityManagerInterface;
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;
class AccommodationController extends AbstractController
{
public function __construct(
private readonly AccommodationRepository $accommodationRepository,
private readonly CmsDataProvider $cmsDataProvider,
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
}
#[Route('/admin/accommodation', name: 'app_admin_accommodation')]
public function index(): Response
{
$accommodations = $this->accommodationRepository->findAll();
return $this->render('admin/accommodation/index.html.twig', [
'accommodations' => $accommodations,
]);
}
#[Route('/admin/accommodation/create', name: 'app_admin_accommodation_create')]
public function create(Request $request): Response
{
$accommodation = new Accommodation();
$form = $this->createForm(AccommodationType::class, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($accommodation);
$this->entityManager->flush();
$this->addFlash('success', 'Das Gruppenhaus wurde angelegt');
$this->logger->info('Created accommodation', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
]);
return $this->redirectToRoute('app_admin_accommodation');
}
return $this->render('admin/accommodation/create.html.twig', [
'form' => $form,
]);
}
#[Route('/admin/accommodation/{id}/edit', name: 'app_admin_accommodation_edit')]
public function edit(Accommodation $accommodation, Request $request): Response
{
// fetch additional data from cms if available
$cmsData = [];
if (null !== $accommodation->getCode()) {
$cmsData = $this->cmsDataProvider->getHotelDetails($accommodation->getCode());
}
$form = $this->createForm(AccommodationType::class, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($accommodation);
$this->entityManager->flush();
$this->addFlash('success', 'Das Gruppenhaus wurde aktualisiert');
$this->logger->info('Updated accommodation', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
]);
return $this->redirectToRoute('app_admin_accommodation_edit', ['id' => $accommodation->getId()]);
}
return $this->render('admin/accommodation/edit.html.twig', [
'accommodation' => $accommodation,
'form' => $form,
'cmsData' => $cmsData,
]);
}
#[Route('/admin/accommodation/{id}/delete', name: 'app_admin_accommodation_delete')]
public function delete(Accommodation $accommodation, Request $request): Response
{
if (true === $request->isMethod(Request::METHOD_POST)) {
$this->entityManager->remove($accommodation);
$this->entityManager->flush();
$this->addFlash('success', 'Das Gruppenhaus wurde gelöscht');
$this->logger->info('Deleted accommodation', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
]);
return new HxRedirectResponse($this->generateUrl('app_admin_accommodation'));
}
return $this->render('admin/accommodation/modal_delete.html.twig', [
'accommodation' => $accommodation,
]);
}
}
@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationPrice;
use App\Form\Admin\Groups\AccommodationPriceType;
use App\Htmx\HxRedirectResponse;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class AccommodationPriceController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
}
#[Route('/admin/accommodation-price/{id}/create', name: 'app_admin_accommodationprice_create')]
public function create(Accommodation $accommodation, Request $request): Response
{
$accommodationPrice = new AccommodationPrice();
$form = $this->getForm($accommodationPrice, null);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$accommodation->addAccommodationPrice($accommodationPrice);
$this->entityManager->persist($accommodationPrice);
$this->entityManager->flush();
$this->addFlash('success', 'Der Preis wurde angelegt');
$this->logger->info('Created accommodation price', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'date_range' => [
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
]
]);
return $this->redirectToRoute('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
}
return $this->render('admin/accommodation_price/create.html.twig', [
'accommodation' => $accommodation,
'form' => $form,
]);
}
#[Route('/admin/accommodation-price/{id}/edit', name: 'app_admin_accommodationprice_edit', defaults: ['duplicate' => false])]
#[Route('/admin/accommodation-price/{id}/duplicate', name: 'app_admin_accommodationprice_duplicate', defaults: ['duplicate' => true])]
public function edit(AccommodationPrice $accommodationPrice, bool $duplicate, Request $request): Response
{
$accommodation = $accommodationPrice->getAccommodation();
if (true === $duplicate) {
$accommodationPrice = clone $accommodationPrice;
}
$form = $this->getForm($accommodationPrice, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if (true === $duplicate) {
$accommodation->addAccommodationPrice($accommodationPrice);
}
$this->entityManager->persist($accommodationPrice);
$this->entityManager->flush();
$this->addFlash('success', 'Der Preis wurde aktualisiert');
$this->logger->info('Edited accommodation price', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'date_range' => [
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
]
]);
return $this->redirectToRoute('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
}
return $this->render('admin/accommodation_price/edit.html.twig', [
'accommodation' => $accommodation,
'accommodationPrice' => $accommodationPrice,
'form' => $form,
'duplicate' => $duplicate,
]);
}
#[Route('/admin/accommodation-price/{id}/delete', name: 'app_admin_accommodationprice_delete')]
public function delete(AccommodationPrice $accommodationPrice, Request $request): Response
{
$accommodation = $accommodationPrice->getAccommodation();
if (true === $request->isMethod(Request::METHOD_POST)) {
$this->entityManager->remove($accommodationPrice);
$this->entityManager->flush();
$this->addFlash('success', 'Der Preis wurde gelöscht');
$this->logger->info('Deleted accommodation price', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'date_range' => [
'from' => $accommodationPrice->getDateFrom()->format('Y-m-d'),
'to' => $accommodationPrice->getDateTo()->format('Y-m-d'),
]
]);
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
return new HxRedirectResponse($returnUrl);
}
return $this->render('admin/accommodation_price/modal_delete.html.twig', [
'accommodationPrice' => $accommodationPrice,
'accommodation' => $accommodation,
]);
}
private function getForm(AccommodationPrice $accommodationPrice, ?Accommodation $accommodation): FormInterface
{
return $this->createForm(AccommodationPriceType::class, $accommodationPrice, ['currency' => $accommodation->getCurrency()]);
}
}
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AdditionalService;
use App\Form\Admin\Groups\AdditionalServiceType;
use App\Htmx\HxRedirectResponse;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class AdditionalServiceController extends AbstractController
{
use SortingControllerTrait;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
}
#[Route('/admin/additional-service/{id}/create', name: 'app_admin_additionalservice_create')]
public function create(Accommodation $accommodation, Request $request): Response
{
$additionalService = new AdditionalService();
$form = $this->getForm($additionalService, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$accommodation->addAdditionalService($additionalService);
$this->entityManager->persist($additionalService);
$this->entityManager->flush();
$this->addFlash('success', 'Die Zusatzleistung wurde angelegt');
$this->logger->info('Created additional service', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'label' => $additionalService->getLabel(),
]);
return $this->redirectToRoute('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
}
return $this->render('admin/additional_service/create.html.twig', [
'accommodation' => $accommodation,
'form' => $form,
]);
}
#[Route('/admin/additional-service/{id}/edit', name: 'app_admin_additionalservice_edit', defaults: ['duplicate' => false])]
#[Route('/admin/additional-service/{id}/duplicate', name: 'app_admin_additionalservice_duplicate', defaults: ['duplicate' => true])]
public function edit(AdditionalService $additionalService, bool $duplicate, Request $request): Response
{
$accommodation = $additionalService->getAccommodation();
if (true === $duplicate) {
$additionalService = clone $additionalService;
}
$form = $this->getForm($additionalService, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if (true === $duplicate) {
$accommodation->addAdditionalService($additionalService);
}
$this->entityManager->persist($additionalService);
$this->entityManager->flush();
$this->addFlash('success', 'Die Zusatzleistung wurde aktualisiert');
$this->logger->info('Edited additional service', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'label' => $additionalService->getLabel(),
]);
return $this->redirectToRoute('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
}
return $this->render('admin/additional_service/edit.html.twig', [
'accommodation' => $accommodation,
'additionalService' => $additionalService,
'form' => $form,
'duplicate' => $duplicate,
]);
}
#[Route('/admin/additional-service/{id}/delete', name: 'app_admin_additionalservice_delete')]
public function delete(AdditionalService $additionalService, Request $request): Response
{
$accommodation = $additionalService->getAccommodation();
if (true === $request->isMethod(Request::METHOD_POST)) {
$this->entityManager->remove($additionalService);
$this->entityManager->flush();
$this->addFlash('success', 'Die Zusatzleistung wurde gelöscht');
$this->logger->info('Deleted additional service', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'label' => $additionalService->getLabel(),
]);
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
return new HxRedirectResponse($returnUrl);
}
return $this->render('admin/additional_service/modal_delete.html.twig', [
'additionalService' => $additionalService,
'accommodation' => $accommodation,
]);
}
#[Route('/admin/additional-service/sort/{id}', name: 'app_admin_additionalservice_sort', methods: ['POST'])]
public function sort(Accommodation $accommodation, Request $request): Response
{
$this->applySorting($accommodation->getAdditionalServices(), $request->request->all('sorting'));
$this->entityManager->flush();
return new Response('', Response::HTTP_NO_CONTENT);
}
private function getForm(AdditionalService $additionalService, ?Accommodation $accommodation): FormInterface
{
return $this->createForm(AdditionalServiceType::class, $additionalService, ['currency' => $accommodation->getCurrency()]);
}
}
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\BoardService;
use App\Form\Admin\Groups\BoardServiceType;
use App\Htmx\HxRedirectResponse;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class BoardServiceController extends AbstractController
{
use SortingControllerTrait;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger,
) {
}
#[Route('/admin/board-service/{id}/create', name: 'app_admin_boardservice_create')]
public function create(Accommodation $accommodation, Request $request): Response
{
$boardService = new BoardService();
$form = $this->getForm($boardService, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$accommodation->addBoardService($boardService);
$this->entityManager->persist($boardService);
$this->entityManager->flush();
$this->addFlash('success', 'Die Verpflegungsleistung wurde angelegt');
$this->logger->info('Created board service', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'label' => $boardService->getLabel(),
]);
return $this->redirectToRoute('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
}
return $this->render('admin/board_service/create.html.twig', [
'accommodation' => $accommodation,
'form' => $form,
]);
}
#[Route('/admin/board-service/{id}/edit', name: 'app_admin_boardservice_edit', defaults: ['duplicate' => false])]
#[Route('/admin/board-service/{id}/duplicate', name: 'app_admin_boardservice_duplicate', defaults: ['duplicate' => true])]
public function edit(BoardService $boardService, bool $duplicate, Request $request): Response
{
$accommodation = $boardService->getAccommodation();
if (true === $duplicate) {
$boardService = clone $boardService;
}
$form = $this->getForm($boardService, $accommodation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if (true === $duplicate) {
$accommodation->addBoardService($boardService);
}
$this->entityManager->persist($boardService);
$this->entityManager->flush();
$this->addFlash('success', 'Die Verpflegungsleistung wurde aktualisiert');
$this->logger->info('Edited board service', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'label' => $boardService->getLabel(),
]);
return $this->redirectToRoute('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
}
return $this->render('admin/board_service/edit.html.twig', [
'accommodation' => $accommodation,
'boardService' => $boardService,
'form' => $form,
'duplicate' => $duplicate,
]);
}
#[Route('/admin/board-service/{id}/delete', name: 'app_admin_boardservice_delete')]
public function delete(BoardService $boardService, Request $request): Response
{
$accommodation = $boardService->getAccommodation();
if (true === $request->isMethod(Request::METHOD_POST)) {
$this->entityManager->remove($boardService);
$this->entityManager->flush();
$this->addFlash('success', 'Die Verpflegungsleistung wurde gelöscht');
$this->logger->info('Deleted board service', [
'name' => $accommodation->getName(),
'code' => $accommodation->getCode(),
'label' => $boardService->getLabel(),
]);
$returnUrl = $this->generateUrl('app_admin_accommodation_edit', [
'id' => $accommodation->getId(),
]);
return new HxRedirectResponse($returnUrl);
}
return $this->render('admin/board_service/modal_delete.html.twig', [
'boardService' => $boardService,
'accommodation' => $accommodation,
]);
}
#[Route('/admin/board-service/sort/{id}', name: 'app_admin_boardservice_sort', methods: ['POST'])]
public function sort(Accommodation $accommodation, Request $request): Response
{
$this->applySorting($accommodation->getBoardServices(), $request->request->all('sorting'));
$this->entityManager->flush();
return new Response('', Response::HTTP_NO_CONTENT);
}
private function getForm(BoardService $boardService, ?Accommodation $accommodation): FormInterface
{
return $this->createForm(BoardServiceType::class, $boardService, ['currency' => $accommodation->getCurrency()]);
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Groups\SortableEntityInterface;
trait SortingControllerTrait
{
private function applySorting(iterable $collection, array $sortingMap): void
{
foreach ($collection as $entity) {
/** @var SortableEntityInterface $entity */
if (isset($sortingMap[$entity->getId()])) {
$entity->setSorting((int) $sortingMap[$entity->getId()]);
}
}
}
}
+13 -3
View File
@@ -10,6 +10,7 @@ use App\Repository\Groups\AccommodationRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: AccommodationRepository::class)]
class Accommodation implements BlameableEntityInterface, TimestampableEntityInterface
@@ -23,36 +24,45 @@ class Accommodation implements BlameableEntityInterface, TimestampableEntityInte
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'required')]
private ?string $name = null;
#[ORM\Column(length: 16, unique: true)]
#[Assert\NotBlank(message: 'required')]
private ?string $code = null;
#[ORM\Column]
#[Assert\NotNull(message: 'required')]
private ?int $minPaxSummer = null;
/**
* @var Collection<int, AccommodationPrice>
*/
#[ORM\OneToMany(targetEntity: AccommodationPrice::class, mappedBy: 'accommodation')]
#[ORM\OneToMany(targetEntity: AccommodationPrice::class, mappedBy: 'accommodation', cascade: ['all'])]
#[ORM\OrderBy(['dateFrom' => 'ASC'])]
private Collection $accommodationPrices;
/**
* @var Collection<int, BoardService>
*/
#[ORM\OneToMany(targetEntity: BoardService::class, mappedBy: 'accommodation')]
#[ORM\OneToMany(targetEntity: BoardService::class, mappedBy: 'accommodation', cascade: ['all'])]
#[ORM\OrderBy(['sorting' => 'ASC'])]
private Collection $boardServices;
/**
* @var Collection<int, AdditionalService>
*/
#[ORM\OneToMany(targetEntity: AdditionalService::class, mappedBy: 'accommodation')]
#[ORM\OneToMany(targetEntity: AdditionalService::class, mappedBy: 'accommodation', cascade: ['all'])]
#[ORM\OrderBy(['sorting' => 'ASC'])]
private Collection $additionalServices;
#[ORM\Column]
#[Assert\NotNull(message: 'required')]
private ?int $maxAdolescentAge = null;
#[ORM\Column(length: 3)]
#[Assert\NotBlank(message: 'required')]
#[Assert\Choice(choices: ['EUR', 'CHF'], message: 'invalid')]
private ?string $currency = 'EUR';
public function __construct()
+90
View File
@@ -9,6 +9,8 @@ use App\Entity\TimestampableEntityInterface;
use App\Repository\Groups\AccommodationPriceRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Entity(repositoryClass: AccommodationPriceRepository::class)]
class AccommodationPrice implements BlameableEntityInterface, TimestampableEntityInterface
@@ -26,23 +28,44 @@ class AccommodationPrice implements BlameableEntityInterface, TimestampableEntit
private ?Accommodation $accommodation = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
#[Assert\NotNull(message: 'required')]
private ?\DateTimeImmutable $dateFrom = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
#[Assert\NotNull(message: 'required')]
#[Assert\GreaterThan(propertyPath: 'dateFrom', message: 'Das Bis-Datum muss nach dem Von-Datum liegen')]
private ?\DateTimeImmutable $dateTo = null;
#[ORM\Column]
#[Assert\NotNull(message: 'required')]
private ?int $includedPax = null;
#[ORM\Column]
#[Assert\NotNull(message: 'required')]
private ?int $pricePerNight = null;
#[ORM\Column]
#[Assert\NotNull(message: 'required')]
private ?int $priceAdditionalPerson = null;
#[ORM\Column]
#[Assert\NotNull(message: 'required')]
private ?int $minNights = null;
#[ORM\Column(nullable: true)]
private ?int $pricePerNightOverride = null;
#[ORM\Column(nullable: true)]
private ?int $priceAdditionalPersonOverride = null;
#[ORM\Column]
private bool $acceptUndersubscription = false;
public function __clone()
{
$this->id = null;
}
public function getId(): ?int
{
return $this->id;
@@ -131,4 +154,71 @@ class AccommodationPrice implements BlameableEntityInterface, TimestampableEntit
return $this;
}
public function getPricePerNightOverride(): ?int
{
return $this->pricePerNightOverride;
}
public function setPricePerNightOverride(?int $pricePerNightOverride): self
{
$this->pricePerNightOverride = $pricePerNightOverride;
return $this;
}
public function getEffectivePricePerNight(): int
{
return $this->pricePerNightOverride ?? $this->pricePerNight;
}
public function getPriceAdditionalPersonOverride(): ?int
{
return $this->priceAdditionalPersonOverride;
}
public function setPriceAdditionalPersonOverride(?int $priceAdditionalPersonOverride): self
{
$this->priceAdditionalPersonOverride = $priceAdditionalPersonOverride;
return $this;
}
public function getEffectivePriceAdditionalPerson(): int
{
return $this->priceAdditionalPersonOverride ?? $this->priceAdditionalPerson;
}
public function isAcceptUndersubscription(): bool
{
return $this->acceptUndersubscription;
}
public function setAcceptUndersubscription(bool $acceptUndersubscription): self
{
$this->acceptUndersubscription = $acceptUndersubscription;
return $this;
}
#[Assert\Callback]
public function assertDatesValid(ExecutionContextInterface $context): void
{
// we only check new entities
if (null !== $this->getId()) {
return;
}
// come on, we need something to work with
if (null === $this->getDateFrom() || null === $this->getDateTo()) {
return;
}
if ($this->getDateFrom() < new \DateTimeImmutable()) {
$context
->buildViolation('Das Von-Datum darf nicht in der Vergangenheit liegen')
->atPath('dateFrom')
->addViolation();
}
}
}
+12 -7
View File
@@ -12,7 +12,7 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AdditionalServiceRepository::class)]
class AdditionalService implements BlameableEntityInterface, TimestampableEntityInterface
class AdditionalService implements BlameableEntityInterface, TimestampableEntityInterface, SortableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
@@ -41,12 +41,17 @@ class AdditionalService implements BlameableEntityInterface, TimestampableEntity
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
private ?\DateTimeImmutable $dateTo = null;
#[ORM\Column(length: 128)]
private ?string $grouping = null;
#[ORM\Column(length: 128, nullable: true)]
private ?string $selectionGroup = null;
#[ORM\Column]
private ?int $sorting = 0;
public function __clone()
{
$this->id = null;
}
public function getId(): ?int
{
return $this->id;
@@ -124,14 +129,14 @@ class AdditionalService implements BlameableEntityInterface, TimestampableEntity
return $this;
}
public function getGrouping(): ?string
public function getSelectionGroup(): ?string
{
return $this->grouping;
return $this->selectionGroup;
}
public function setGrouping(?string $grouping): self
public function setSelectionGroup(?string $selectionGroup): self
{
$this->grouping = $grouping;
$this->selectionGroup = $selectionGroup;
return $this;
}
+6 -1
View File
@@ -11,7 +11,7 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BoardServiceRepository::class)]
class BoardService implements BlameableEntityInterface, TimestampableEntityInterface
class BoardService implements BlameableEntityInterface, TimestampableEntityInterface, SortableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
@@ -40,6 +40,11 @@ class BoardService implements BlameableEntityInterface, TimestampableEntityInter
#[ORM\Column]
private ?int $sorting = 0;
public function __clone()
{
$this->id = null;
}
public function getId(): ?int
{
return $this->id;
@@ -0,0 +1,10 @@
<?php
namespace App\Entity\Groups;
interface SortableEntityInterface
{
public function getId(): ?int;
public function setSorting(?int $sorting): self;
}
@@ -8,4 +8,9 @@ enum AdditionalServiceType: string
case PerPerson = 'per_person';
case PerNight = 'per_night';
case PerPersonPerNight = 'per_person_per_night';
public function label(): string
{
return 'enum.additional_service.'.$this->value;
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\AccommodationPrice;
use App\Form\DatepickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AccommodationPriceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('dateFrom', DatepickerType::class, [
'label' => 'Datum von',
'html5' => true,
'widget' => 'single_text',
])
->add('dateTo', DatepickerType::class, [
'label' => 'Datum bis',
'html5' => true,
'widget' => 'single_text',
])
->add('includedPax', IntegerType::class, [
'label' => 'Inklusiv-Personen',
])
->add('minNights', IntegerType::class, [
'label' => 'Mindestbelegung (Nächte)',
])
->add('pricePerNight', MoneyType::class, [
'label' => 'Preis pro Nacht',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('pricePerNightOverride', MoneyType::class, [
'label' => 'Streich-Preis pro Nacht',
'required' => false,
'currency' => $options['currency'],
'divisor' => 100,
])
->add('priceAdditionalPerson', MoneyType::class, [
'label' => 'Preis weitere Person',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('priceAdditionalPersonOverride', MoneyType::class, [
'label' => 'Streich-Preis weitere Person',
'required' => false,
'currency' => $options['currency'],
'divisor' => 100,
])
->add('acceptUndersubscription', CheckboxType::class, [
'label' => 'Unterbelegung akzeptieren',
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AccommodationPrice::class,
'currency' => 'EUR',
]);
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\Accommodation;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AccommodationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'Name',
])
->add('code', TextType::class, [
'label' => 'Code',
])
->add('minPaxSummer', IntegerType::class, [
'label' => 'Mindestbelegung Sommer',
])
->add('maxAdolescentAge', IntegerType::class, [
'label' => 'Altersgrenze Jugendliche (6-X Jahre)',
])
->add('currency', ChoiceType::class, [
'label' => 'Währung',
'choices' => [
'EUR' => 'EUR',
'CHF' => 'CHF',
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Accommodation::class,
]);
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\AdditionalService;
use App\Enum\Groups\AdditionalServiceType as AdditionalServiceTypeEnum;
use App\Form\DatepickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AdditionalServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TextType::class, [
'label' => 'Bezeichnung',
])
->add('type', EnumType::class, [
'label' => 'Preistyp',
'class' => AdditionalServiceTypeEnum::class,
'choice_label' => fn($item) => $item->label(),
])
->add('price', MoneyType::class, [
'label' => 'Preis',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('selectionGroup', TextType::class, [
'label' => 'Auswahlgruppe',
'required' => false,
])
->add('dateFrom', DatepickerType::class, [
'label' => 'Datum von',
'html5' => true,
'widget' => 'single_text',
])
->add('dateTo', DatepickerType::class, [
'label' => 'Datum bis',
'html5' => true,
'widget' => 'single_text',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => AdditionalService::class,
'currency' => 'EUR',
]);
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Form\Admin\Groups;
use App\Entity\Groups\BoardService;
use App\Form\DatepickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BoardServiceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('label', TextType::class, [
'label' => 'Bezeichnung',
])
->add('price', MoneyType::class, [
'label' => 'Preis pro Person/Nacht',
'currency' => $options['currency'],
'divisor' => 100,
])
->add('dateFrom', DatepickerType::class, [
'label' => 'Datum von',
'html5' => true,
'widget' => 'single_text',
])
->add('dateTo', DatepickerType::class, [
'label' => 'Datum bis',
'html5' => true,
'widget' => 'single_text',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BoardService::class,
'currency' => 'EUR',
]);
$resolver->setAllowedValues('currency', ['EUR', 'CHF']);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DatepickerType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'html5' => false,
'widget' => 'single_text',
'input' => 'datetime_immutable',
'min_date' => null,
'max_date' => null,
'disable_weekends' => false,
]);
$resolver->setAllowedTypes('min_date', ['null', \DateTimeImmutable::class]);
$resolver->setAllowedTypes('max_date', ['null', \DateTimeImmutable::class]);
$resolver->setAllowedTypes('disable_weekends', 'bool');
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['min_date'] = $options['min_date'];
$view->vars['max_date'] = $options['max_date'];
$view->vars['disable_weekends'] = $options['disable_weekends'];
}
public function getParent(): string
{
return DateType::class;
}
}
+12
View File
@@ -29,6 +29,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
],
'extras' => [
'icon' => 'edit',
'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']],
],
]);
$menu->addChild('Benutzer', [
@@ -40,6 +41,16 @@ class AdminMenuBuilder extends AbstractMenuBuilder
'icon' => 'users',
],
]);
$menu->addChild('Gruppenhäuser', [
'route' => 'app_admin_accommodation',
'linkAttributes' => [
'title' => 'Gruppenhäuser',
],
'extras' => [
'icon' => 'house',
'routes' => [['pattern' => '/^app_admin_accommodation/']],
],
]);
$menu->addChild('Logs', [
'route' => 'app_admin_log',
'linkAttributes' => [
@@ -47,6 +58,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
],
'extras' => [
'icon' => 'list',
'routes' => [['pattern' => '/^app_admin_log/']],
],
]);
+95 -25
View File
@@ -2,13 +2,18 @@
namespace App\Service;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class CmsDataProvider
{
public function __construct(private readonly HttpClientInterface $httpClient, private readonly string $apiKey)
{
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly CacheInterface $cache,
private readonly string $apiKey,
) {
}
/**
@@ -27,33 +32,98 @@ class CmsDataProvider
return $result['hotel']['images'] ?? null;
}
/**
* @return array<string, mixed>|null
*/
public function getHotelImages(string $hotelCode): ?array
{
$result = $this->getHotelDetails($hotelCode);
if (true === isset($result['success']) && false === $result['success']) {
return null;
}
return $result['images'] ?? null;
}
/** @return array<string, mixed> */
public function getHotelDetails(string $hotelCode): array
{
return $this->cache->get(
sprintf('cms_hotel_%s', $hotelCode),
function (ItemInterface $item) use ($hotelCode): array {
$item->expiresAfter(3600);
try {
$request = $this->httpClient->request('GET', 'api/hotel', [
'query' => [
'hotel' => $hotelCode,
'key' => $this->apiKey,
],
]);
} catch (ExceptionInterface $e) {
$item->expiresAfter(0);
return [
'success' => false,
'message' => $e->getMessage(),
];
}
try {
$data = $request->toArray();
} catch (ExceptionInterface $e) {
$item->expiresAfter(0);
return [
'success' => false,
'message' => $e->getMessage(),
];
}
return $data;
}
);
}
/** @return array<string, mixed> */
public function getProductDetails(string $productCode, ?string $hotelCode = null): array
{
try {
$request = $this->httpClient->request('GET', 'api/product', [
'query' => [
'product' => $productCode,
'hotel' => $hotelCode,
'key' => $this->apiKey,
],
]);
} catch (ExceptionInterface $e) {
return [
'success' => false,
'message' => $e->getMessage(),
];
}
return $this->cache->get(
sprintf('cms_product_%s_%s', $productCode, $hotelCode ?? ''),
function (ItemInterface $item) use ($productCode, $hotelCode): array {
$item->expiresAfter(3600);
try {
$data = $request->toArray();
} catch (ExceptionInterface $e) {
return [
'success' => false,
'message' => $e->getMessage(),
];
}
try {
$request = $this->httpClient->request('GET', 'api/product', [
'query' => [
'product' => $productCode,
'hotel' => $hotelCode,
'key' => $this->apiKey,
],
]);
} catch (ExceptionInterface $e) {
$item->expiresAfter(0);
return $data;
return [
'success' => false,
'message' => $e->getMessage(),
];
}
try {
$data = $request->toArray();
} catch (ExceptionInterface $e) {
$item->expiresAfter(0);
return [
'success' => false,
'message' => $e->getMessage(),
];
}
return $data;
}
);
}
}