diff --git a/migrations/Version20240515131333.php b/migrations/Version20240515131333.php new file mode 100644 index 0000000..c996bd1 --- /dev/null +++ b/migrations/Version20240515131333.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE destination ADD created_by VARCHAR(255) DEFAULT NULL, ADD updated_by VARCHAR(255) DEFAULT NULL, CHANGE bus_pro_id bus_pro_id INT DEFAULT NULL, CHANGE code code VARCHAR(32) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE destination DROP created_by, DROP updated_by, CHANGE bus_pro_id bus_pro_id INT NOT NULL, CHANGE code code VARCHAR(32) NOT NULL'); + } +} diff --git a/src/Controller/Admin/Autocomplete/HotelController.php b/src/Controller/Admin/Autocomplete/HotelController.php new file mode 100644 index 0000000..21ca197 --- /dev/null +++ b/src/Controller/Admin/Autocomplete/HotelController.php @@ -0,0 +1,50 @@ +getContent(), true, 512, JSON_THROW_ON_ERROR); + $queryString = $query['search']; + } catch (\JsonException $e) { + throw $this->createNotFoundException(); + } + + /** @var Hotel[] $hotels */ + $hotels = $this->hotelDataProvider->getAll(); + + $data = [ + [ + 'value' => '', + 'text' => '...', + ], + ]; + + foreach ($hotels as $hotel) { + if (false !== stripos($hotel->getName(), $queryString)) { + $data[] = [ + 'value' => $hotel->getBusProId(), + 'text' => sprintf('%s (%s)', $hotel->getName(), $hotel->getCode()), + ]; + } + } + + return $this->json($data); + } +} \ No newline at end of file diff --git a/src/Controller/Administrative/System/Destination/CreateController.php b/src/Controller/Administrative/System/Destination/CreateController.php new file mode 100644 index 0000000..2927f5e --- /dev/null +++ b/src/Controller/Administrative/System/Destination/CreateController.php @@ -0,0 +1,60 @@ +createForm(DestinationType::class, $destinationDto); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $destination = new Destination(); + $this->updateEntity($destination, $destinationDto); + + $this->entityManager->persist($destination); + $this->entityManager->flush(); + + $this->addFlash('success', 'Die Reise wurde angelegt'); + + $this->logger->info('Create destination', [ + 'destination_id' => $destination->getId(), + 'destination' => (string) $destination, + ]); + + return $this->redirectToRoute('app_administrative_system_destination_index'); + } + + return $this->render('administrative/system/destination/create.html.twig', [ + 'form' => $form->createView(), + ]); + } +} \ No newline at end of file diff --git a/src/Controller/Administrative/System/Destination/DeleteController.php b/src/Controller/Administrative/System/Destination/DeleteController.php new file mode 100644 index 0000000..8b87660 --- /dev/null +++ b/src/Controller/Administrative/System/Destination/DeleteController.php @@ -0,0 +1,45 @@ +isMethod('POST')) { + $destination->setDeleted(); + $this->entityManager->flush(); + + $this->logger->info('Delete destination', [ + 'destination_id' => $destination->getId(), + 'destination' => (string) $destination, + ]); + + $this->addFlash('success', 'Die Reise wurde gelöscht'); + + return new HxRedirectResponse($this->generateUrl('app_administrative_system_destination_index')); + } + + return $this->render('administrative/system/destination/modal_delete.html.twig', [ + 'destination' => $destination, + ]); + } +} \ No newline at end of file diff --git a/src/Controller/Administrative/System/Destination/DestinationTrait.php b/src/Controller/Administrative/System/Destination/DestinationTrait.php new file mode 100644 index 0000000..b1937c0 --- /dev/null +++ b/src/Controller/Administrative/System/Destination/DestinationTrait.php @@ -0,0 +1,42 @@ +getPickups() as $pickupId) { + $pickup = $this->pickupDataProvider->get($pickupId); + $pickups[] = [ + 'busProId' => $pickup->getBusProId(), + 'city' => $pickup->getCity(), + 'street' => $pickup->getStreet(), + 'time' => $pickup->getTime(), + ]; + } + + return $pickups; + } + + private function updateEntity(Destination $destination, DestinationDto $destinationDto): void + { + $hotel = $this->hotelDataProvider->get($destinationDto->getHotelBusProId()); + $pickups = $this->mapPickups($destinationDto); + + $destination + ->setProduct($destinationDto->getProduct()) + ->setDateFrom($destinationDto->getDateFrom()) + ->setDateTo($destinationDto->getDateTo()) + ->setHotel($hotel->getName()) + ->setHotelCode($hotel->getCode()) + ->setHotelBusProId($hotel->getBusProId()) + ->setPickups($pickups) + ; + } +} \ No newline at end of file diff --git a/src/Controller/Administrative/System/Destination/DuplicateController.php b/src/Controller/Administrative/System/Destination/DuplicateController.php new file mode 100644 index 0000000..436419a --- /dev/null +++ b/src/Controller/Administrative/System/Destination/DuplicateController.php @@ -0,0 +1,68 @@ +createForm(DestinationType::class, $destinationDto); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $hotel = $this->hotelDataProvider->get($destinationDto->getHotelBusProId()); + + $copy = new Destination(); + $copy + ->setProduct($destinationDto->getProduct()) + ->setDateFrom($destinationDto->getDateFrom()) + ->setDateTo($destinationDto->getDateTo()) + ->setHotel($hotel->getName()) + ->setHotelCode($hotel->getCode()) + ->setHotelBusProId($hotel->getBusProId()) + ; + + $this->entityManager->persist($copy); + $this->entityManager->flush(); + + $this->addFlash('success', 'Die Reise wurde dupliziert'); + + $this->logger->info('Duplicate destination', [ + 'original_id' => $destination->getId(), + 'duplicate_id' => $copy->getId(), + 'destination' => (string) $destination, + ]); + + return $this->redirectToRoute('app_administrative_system_destination_edit', [ + 'id' => $copy->getId(), + ]); + } + + return $this->render('administrative/system/destination/duplicate.html.twig', [ + 'form' => $form->createView(), + ]); + } +} \ No newline at end of file diff --git a/src/Controller/Administrative/System/Destination/EditController.php b/src/Controller/Administrative/System/Destination/EditController.php new file mode 100644 index 0000000..177c208 --- /dev/null +++ b/src/Controller/Administrative/System/Destination/EditController.php @@ -0,0 +1,55 @@ +createForm(DestinationType::class, $destinationDto); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->updateEntity($destination, $destinationDto); + + $this->entityManager->flush(); + + $this->addFlash('success', 'Die Reise wurde aktualisiert'); + + $this->logger->info('Edit assignment', [ + 'destination_id' => $destination->getId(), + 'destination' => (string) $destination, + ]); + + return $this->redirectToRoute('app_administrative_system_destination_index'); + } + + return $this->render('administrative/system/destination/edit.html.twig', [ + 'form' => $form->createView(), + ]); + } +} \ No newline at end of file diff --git a/src/Controller/Administrative/System/Destination/IndexController.php b/src/Controller/Administrative/System/Destination/IndexController.php new file mode 100644 index 0000000..3ee9024 --- /dev/null +++ b/src/Controller/Administrative/System/Destination/IndexController.php @@ -0,0 +1,44 @@ +destinationRepository + ->getListQueryForCustom() + ; + + $pagination = $this->paginator->paginate( + $query, + $request->query->getInt('page', 1), + 10, + [ + 'defaultSortFieldName' => 'destination.dateFrom', + 'defaultSortDirection' => 'desc', + ] + ); + + return $this->render('administrative/system/destination/index.html.twig', [ + 'pagination' => $pagination, + ]); + } +} \ No newline at end of file diff --git a/src/Entity/Destination.php b/src/Entity/Destination.php index 3666145..7b5047e 100644 --- a/src/Entity/Destination.php +++ b/src/Entity/Destination.php @@ -3,15 +3,18 @@ namespace App\Entity; use App\BusProNet\Model\Pickup; +use App\Entity\Traits\BlameableEntity; use App\Entity\Traits\SoftDeletableEntity; use App\Entity\Traits\TimestampableEntity; use App\Repository\DestinationRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: DestinationRepository::class)] -class Destination implements TimestampableEntityInterface, SoftDeletableEntityInterface +class Destination implements TimestampableEntityInterface, SoftDeletableEntityInterface, BlameableEntityInterface { + use BlameableEntity; use TimestampableEntity; use SoftDeletableEntity; @@ -20,10 +23,10 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn #[ORM\Column] private ?int $id = null; - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $busProId = null; - #[ORM\Column(length: 32)] + #[ORM\Column(length: 32, nullable: true)] private ?string $code = null; #[ORM\Column(type: Types::DATE_IMMUTABLE)] @@ -81,7 +84,7 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn return $this->dateFrom; } - public function setDateFrom(\DateTimeImmutable $dateFrom): static + public function setDateFrom(?\DateTimeImmutable $dateFrom): static { $this->dateFrom = $dateFrom; @@ -93,7 +96,7 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn return $this->dateTo; } - public function setDateTo(\DateTimeImmutable $dateTo): static + public function setDateTo(?\DateTimeImmutable $dateTo): static { $this->dateTo = $dateTo; @@ -117,7 +120,7 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn return $this->product; } - public function setProduct(string $product): static + public function setProduct(?string $product): static { $this->product = $product; @@ -129,7 +132,7 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn return $this->hotel; } - public function setHotel(string $hotel): static + public function setHotel(?string $hotel): static { $this->hotel = $hotel; diff --git a/src/Form/AssignmentType.php b/src/Form/AssignmentType.php index 0788273..dfb87e0 100644 --- a/src/Form/AssignmentType.php +++ b/src/Form/AssignmentType.php @@ -181,7 +181,11 @@ class AssignmentType extends AbstractType $choices = []; foreach ($destination->getPickups() as $pickup) { - $label = sprintf('%s, %s Uhr', trim($pickup['city']), trim($pickup['time'])); + if ($pickup['time']) { + $label = sprintf('%s, %s Uhr', trim($pickup['city']), trim($pickup['time'])); + } else { + $label = trim($pickup['city']); + } $choices[$label] = $pickup['busProId']; } diff --git a/src/Form/AutocompleteChoiceType.php b/src/Form/AutocompleteChoiceType.php index a41e5bd..465ccfd 100644 --- a/src/Form/AutocompleteChoiceType.php +++ b/src/Form/AutocompleteChoiceType.php @@ -23,6 +23,7 @@ class AutocompleteChoiceType extends AbstractType 'placeholder' => null, 'endpoint_parameters' => [], 'controller_action' => null, + 'initial_label' => null, ]); $resolver->setAllowedTypes('choices', 'array'); @@ -37,7 +38,7 @@ class AutocompleteChoiceType extends AbstractType } $view->vars['placeholder'] = $options['placeholder']; - $view->vars['initial_label'] = $form->getData(); + $view->vars['initial_label'] = $options['initial_label'] ?? $form->getData(); $view->vars['initial_value'] = $form->getData(); $view->vars['action'] = $options['controller_action']; } diff --git a/src/Form/BpnPickupType.php b/src/Form/BpnPickupType.php index 3cbc554..05356ca 100644 --- a/src/Form/BpnPickupType.php +++ b/src/Form/BpnPickupType.php @@ -3,7 +3,7 @@ namespace App\Form; use App\BusProNet\DataProvider\PickupDataProvider; -use App\Form\ChoiceLoader\BpnPickupsChoiceLoader; +use App\Form\ChoiceLoader\BpnPickupChoiceLoader; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -21,7 +21,7 @@ class BpnPickupType extends AbstractType public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'choice_loader' => new BpnPickupsChoiceLoader($this->pickups), + 'choice_loader' => new BpnPickupChoiceLoader($this->pickups), 'preferred_choices' => [ 40, // Köln 18, // Essen diff --git a/src/Form/ChoiceLoader/BpnProductsChoiceLoader.php b/src/Form/ChoiceLoader/BpnHotelChoiceLoader.php similarity index 58% rename from src/Form/ChoiceLoader/BpnProductsChoiceLoader.php rename to src/Form/ChoiceLoader/BpnHotelChoiceLoader.php index a929dfe..66203a8 100644 --- a/src/Form/ChoiceLoader/BpnProductsChoiceLoader.php +++ b/src/Form/ChoiceLoader/BpnHotelChoiceLoader.php @@ -2,25 +2,26 @@ namespace App\Form\ChoiceLoader; -use App\BusProNet\DataProvider\ProductDataProvider; -use App\BusProNet\Model\Product; +use App\BusProNet\DataProvider\HotelDataProvider; +use App\BusProNet\Model\Hotel; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; -class BpnProductsChoiceLoader implements ChoiceLoaderInterface +class BpnHotelChoiceLoader implements ChoiceLoaderInterface { - public function __construct(private readonly ProductDataProvider $products) + public function __construct(private readonly HotelDataProvider $dataProvider) {} public function loadChoiceList(callable $value = null): ChoiceListInterface { $choices = []; - /** @var Product[] $products */ - $products = $this->products->getAll(); + /** @var Hotel[] $hotels */ + $hotels = $this->dataProvider->getAll(); - foreach ($products as $product) { - $choices[$product->getName()] = $product->getId(); + foreach ($hotels as $hotel) { + $label = sprintf('%s (%s)', $hotel->getName(), $hotel->getCode()); + $choices[$label] = $hotel->getBusProId(); } ksort($choices); diff --git a/src/Form/ChoiceLoader/BpnPickupsChoiceLoader.php b/src/Form/ChoiceLoader/BpnPickupChoiceLoader.php similarity index 88% rename from src/Form/ChoiceLoader/BpnPickupsChoiceLoader.php rename to src/Form/ChoiceLoader/BpnPickupChoiceLoader.php index e3b0ab4..c218fa5 100644 --- a/src/Form/ChoiceLoader/BpnPickupsChoiceLoader.php +++ b/src/Form/ChoiceLoader/BpnPickupChoiceLoader.php @@ -8,16 +8,16 @@ use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; -class BpnPickupsChoiceLoader implements ChoiceLoaderInterface +class BpnPickupChoiceLoader implements ChoiceLoaderInterface { - public function __construct(private readonly PickupDataProvider $pickups) + public function __construct(private readonly PickupDataProvider $dataProvider) {} public function loadChoiceList(callable $value = null): ChoiceListInterface { $choices = []; /** @var Pickup[] $pickups */ - $pickups = $this->pickups->getAll(); + $pickups = $this->dataProvider->getAll(); foreach ($pickups as $pickup) { $choices[$pickup->getCity()] = $pickup->getBusProId(); diff --git a/src/Form/DestinationType.php b/src/Form/DestinationType.php new file mode 100644 index 0000000..ba48d53 --- /dev/null +++ b/src/Form/DestinationType.php @@ -0,0 +1,80 @@ +add('product', TextType::class, [ + 'label' => 'Bezeichnung', + ]) + ->add('dateFrom', DatepickerType::class, [ + 'label' => 'Datum von', + ]) + ->add('dateTo', DatepickerType::class, [ + 'label' => 'Datum bis', + ]) + ->add('pickups', CollectionType::class, [ + 'label' => 'Zustiege', + 'entry_type' => BpnPickupType::class, + 'allow_add' => true, + 'allow_delete' => true, + ]) + ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + $this->addHotelField($form, $data->getHotelBusProId()); + }) + ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $form = $event->getForm(); + $data = $event->getData(); + + $this->addHotelField($form, $data['hotelBusProId'] ?? null); + }) + ; + } + + private function addHotelField(FormInterface $form, ?string $hotelBusProId): void + { + $hotelLabel = null; + + if (null !== $hotelBusProId) { + $hotel = $this->hotelDataProvider->get($hotelBusProId); + $hotelLabel = sprintf('%s (%s)', $hotel->getName(), $hotel->getCode()); + } + + $form + ->remove('hotelBusProId') + ->add('hotelBusProId', AutocompleteChoiceType::class, [ + 'label' => 'Vertragspartner', + 'endpoint_route' => 'app_admin_autocomplete_hotel', + 'initial_label' => $hotelLabel, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => DestinationDto::class, + ]); + } +} \ No newline at end of file diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php index 462baa2..94b958c 100644 --- a/src/Menu/AdminMenuBuilder.php +++ b/src/Menu/AdminMenuBuilder.php @@ -126,6 +126,17 @@ class AdminMenuBuilder extends AbstractMenuBuilder ], ], ]); + $settingsMenu->addChild('Reisen', [ + 'route' => 'app_administrative_system_destination_index', + 'linkAttributes' => [ + 'title' => 'Reisen', + ], + 'extras' => [ + 'routes' => [ + ['pattern' => '/^app_administrative_system_destination_/'] + ], + ], + ]); $settingsMenu->addChild('Feedbackvorlagen', [ 'route' => 'app_admin_system_feedback_set_index', 'linkAttributes' => [ diff --git a/src/Menu/ManagerMenuBuilder.php b/src/Menu/ManagerMenuBuilder.php index d8d562d..b689d2b 100644 --- a/src/Menu/ManagerMenuBuilder.php +++ b/src/Menu/ManagerMenuBuilder.php @@ -72,6 +72,17 @@ class ManagerMenuBuilder extends AbstractMenuBuilder 'icon' => 'settings', ], ]); + $settingsMenu->addChild('Reisen', [ + 'route' => 'app_administrative_system_destination_index', + 'linkAttributes' => [ + 'title' => 'Reisen', + ], + 'extras' => [ + 'routes' => [ + ['pattern' => '/^app_administrative_system_destination_/'] + ], + ], + ]); $settingsMenu->addChild('FAQ', [ 'route' => 'app_administrative_system_faq_index', 'linkAttributes' => [ diff --git a/src/Model/DestinationDto.php b/src/Model/DestinationDto.php new file mode 100644 index 0000000..e579351 --- /dev/null +++ b/src/Model/DestinationDto.php @@ -0,0 +1,102 @@ +getPickups()); + + $instance + ->setProduct($destination->getProduct()) + ->setDateFrom($destination->getDateFrom()) + ->setDateTo($destination->getDateTo()) + ->setHotelBusProId($destination->getHotelBusProId()) + ->setPickups($pickupIds) + ; + + return $instance; + } + + public function getProduct(): ?string + { + return $this->product; + } + + public function setProduct(?string $product): static + { + $this->product = $product; + + return $this; + } + + public function getDateFrom(): ?\DateTimeImmutable + { + return $this->dateFrom; + } + + public function setDateFrom(?\DateTimeImmutable $dateFrom): static + { + $this->dateFrom = $dateFrom; + + return $this; + } + + public function getDateTo(): ?\DateTimeImmutable + { + return $this->dateTo; + } + + public function setDateTo(?\DateTimeImmutable $dateTo): static + { + $this->dateTo = $dateTo; + + return $this; + } + + public function getHotelBusProId(): ?string + { + return $this->hotelBusProId; + } + + public function setHotelBusProId(?string $hotelBusProId): static + { + $this->hotelBusProId = $hotelBusProId; + + return $this; + } + + public function getPickups(): array + { + return $this->pickups; + } + + public function setPickups(array $pickups): static + { + $this->pickups = $pickups; + + return $this; + } +} \ No newline at end of file diff --git a/src/Model/EmailAttachmentDto.php b/src/Model/EmailAttachmentDto.php index 2766385..4b33484 100644 --- a/src/Model/EmailAttachmentDto.php +++ b/src/Model/EmailAttachmentDto.php @@ -4,8 +4,11 @@ namespace App\Model; class EmailAttachmentDto { - public function __construct(private string $name, private string $content, private string $mimeType) - { + public function __construct( + private readonly string $name, + private readonly string $content, + private readonly string $mimeType + ) { } public function getName(): string diff --git a/src/Model/UploadDto.php b/src/Model/UploadDto.php index e16d34e..7257119 100644 --- a/src/Model/UploadDto.php +++ b/src/Model/UploadDto.php @@ -4,24 +4,13 @@ namespace App\Model; class UploadDto { - private string $uuid; - private string $filename; - private string $originalFilename; - private string $mimeType; - private int $size; - public function __construct( - string $uuid, - string $filename, - string $originalFilename, - string $mimeType, - int $size + private readonly string $uuid, + private readonly string $filename, + private readonly string $originalFilename, + private readonly string $mimeType, + private readonly int $size ) { - $this->uuid = $uuid; - $this->filename = $filename; - $this->originalFilename = $originalFilename; - $this->mimeType = $mimeType; - $this->size = $size; } public function getUuid(): string diff --git a/src/Repository/DestinationRepository.php b/src/Repository/DestinationRepository.php index 195f0cd..913c5a6 100644 --- a/src/Repository/DestinationRepository.php +++ b/src/Repository/DestinationRepository.php @@ -5,6 +5,7 @@ namespace App\Repository; use App\Entity\Destination; use App\Repository\Traits\QueryHelperTrait; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; /** @@ -24,6 +25,20 @@ class DestinationRepository extends ServiceEntityRepository parent::__construct($registry, Destination::class); } + public function getListQueryForCustom(): Query + { + $qb = $this->createQueryBuilder('destination'); + + return $qb + ->where( + $qb->expr()->andX( + $qb->expr()->isNull('destination.deletedAt'), + $qb->expr()->isNull('destination.busProId')) + ) + ->getQuery() + ; + } + public function getAutocompletionData(string $search, bool $upcomingOnly = true): array { $qb = $this->createQueryBuilder('destination'); diff --git a/templates/administrative/system/destination/_form.html.twig b/templates/administrative/system/destination/_form.html.twig new file mode 100644 index 0000000..6584289 --- /dev/null +++ b/templates/administrative/system/destination/_form.html.twig @@ -0,0 +1,45 @@ +{% macro collectionRow(form) %} +
| + {{ knp_pagination_sortable(pagination, 'Produkt', 'destination.product') }} + | ++ {{ knp_pagination_sortable(pagination, 'Vertragspartner', 'destination.hotel') }} + | ++ {{ knp_pagination_sortable(pagination, 'Zeitraum von', 'destination.dateFrom') }} + | ++ {{ knp_pagination_sortable(pagination, 'Zeitraum bis', 'destination.dateTo') }} + | ++ |
|---|---|---|---|---|
| + {{ destination.product }} + | ++ {{ destination.hotel }}{% if destination.hotelCode is not null %} ({{ destination.hotelCode }}){% endif %} + | ++ {{ destination.dateFrom | date('d.m.Y') }} + | ++ {{ destination.dateTo | date('d.m.Y') }} + | +
+ |
+
| + Keine Daten... + | +||||