diff --git a/assets/app.js b/assets/app.js index 9c189e3..114ed3f 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,4 +1,8 @@ -import 'htmx.org'; +import htmx from 'htmx.org' +htmx.config.includeIndicatorStyles = false +htmx.config.historyCacheSize = 0 +htmx.config.allowScriptTags = false + import './bootstrap.js'; /* * Welcome to your app's main JavaScript file! diff --git a/composer.json b/composer.json index e51052c..5e5a0bb 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "oneup/uploader-bundle": "^4.0", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.22", + "scienta/doctrine-json-functions": "^6.3", "setasign/fpdf": "^1.8", "setasign/fpdi": "^2.5", "spatie/icalendar-generator": "^2.5", diff --git a/composer.lock b/composer.lock index eb03003..2349c79 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1e48d4a4ffe643c14cf370b7083adddb", + "content-hash": "804c55640363108581ce07cf9dedaece", "packages": [ { "name": "beberlei/doctrineextensions", @@ -2979,6 +2979,78 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "scienta/doctrine-json-functions", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/ScientaNL/DoctrineJsonFunctions.git", + "reference": "554b2fd281e976a791501fc4753ffd4c5891ec62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ScientaNL/DoctrineJsonFunctions/zipball/554b2fd281e976a791501fc4753ffd4c5891ec62", + "reference": "554b2fd281e976a791501fc4753ffd4c5891ec62", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.2 || ^4", + "doctrine/lexer": "^2.0 || ^3.0", + "doctrine/orm": "^2.19 || ^3", + "ext-pdo": "*", + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0 || ^11.0 || ^12.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-doctrine": "^1.4", + "phpstan/phpstan-phpunit": "^1.4", + "phpunit/phpunit": "^10.1", + "psalm/plugin-phpunit": "^0.18", + "slevomat/coding-standard": "~8", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^5.2", + "webmozart/assert": "^1.11" + }, + "suggest": { + "dunglas/doctrine-json-odm": "To serialize / deserialize objects as JSON documents." + }, + "type": "library", + "autoload": { + "psr-4": { + "Scienta\\DoctrineJsonFunctions\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Doctrine Json Functions Contributors", + "homepage": "https://github.com/ScientaNL/DoctrineJsonFunctions/contributors" + } + ], + "description": "A set of extensions to Doctrine that add support for json query functions.", + "keywords": [ + "database", + "doctrine", + "dql", + "json", + "mariadb", + "mysql", + "orm", + "postgres", + "postgresql", + "sqlite" + ], + "support": { + "issues": "https://github.com/ScientaNL/DoctrineJsonFunctions/issues", + "source": "https://github.com/ScientaNL/DoctrineJsonFunctions/tree/6.3.0" + }, + "time": "2024-11-08T12:33:19+00:00" + }, { "name": "setasign/fpdf", "version": "1.8.6", diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 45975e9..670a00c 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -23,6 +23,7 @@ doctrine: string_functions: DATE_FORMAT: DoctrineExtensions\Query\Mysql\DateFormat FIELD: DoctrineExtensions\Query\Mysql\Field + JSON_CONTAINS: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonContains datetime_functions: DATEDIFF: DoctrineExtensions\Query\Mysql\DateDiff when@test: diff --git a/deploy.php b/deploy.php index 57a47a0..3fd3725 100644 --- a/deploy.php +++ b/deploy.php @@ -80,6 +80,20 @@ host('prod') ->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{release_or_current_path}}/public/ --web-url=https://myep-team.ep-reisen.de') ; +host('staging') + ->setHostname('185.243.132.170') + ->setRemoteUser('p546473') + ->setForwardAgent(true) + ->setSshMultiplexing(true) + ->setDeployPath('/home/www/p546473/html/myep-team') + ->set('writable_mode', 'chmod') + ->set('bin/php', '/usr/local/bin/php') + ->set('rsync_src', __DIR__) + ->set('rsync', $rsyncOptions) + ->set('keep_releases', 5) + ->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{release_or_current_path}}/public/ --web-url=https://myep-team.dreipunktnull.net') +; + task('deploy', [ 'deploy:info', 'deploy:setup', diff --git a/migrations/Version20250107163840.php b/migrations/Version20250107163840.php new file mode 100644 index 0000000..c217f82 --- /dev/null +++ b/migrations/Version20250107163840.php @@ -0,0 +1,46 @@ +addSql('ALTER TABLE user ADD hotel_codes JSON NOT NULL COMMENT \'(DC2Type:json)\''); + } + + public function postUp(Schema $schema): void + { + $this->connection->executeQuery('UPDATE user SET hotel_codes="[]" WHERE hotel_code IS NULL'); + + $rows = $this->connection->fetchAllAssociative('SELECT id, hotel_code FROM user WHERE hotel_code IS NOT NULL'); + + foreach ($rows as $row) { + $this->connection->update('user', [ + 'hotel_codes' => json_encode([$row['hotel_code']]), + ], [ + 'id' => $row['id'], + ]); + } + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user DROP hotel_codes'); + } +} diff --git a/src/BusProNet/DataProvider/HotelDataProvider.php b/src/BusProNet/DataProvider/HotelDataProvider.php index 39e6ee6..49409e4 100644 --- a/src/BusProNet/DataProvider/HotelDataProvider.php +++ b/src/BusProNet/DataProvider/HotelDataProvider.php @@ -43,7 +43,7 @@ class HotelDataProvider $hotels = []; foreach ($this->getAll() as $hotel) { - if (str_starts_with($hotel->getCode(), $code)) { + if (str_starts_with($hotel->getCode(), $code) || str_ends_with($hotel->getCode(), $code)) { $hotels[] = $hotel; } } diff --git a/src/BusProNet/Model/CrmAttributesResponse.php b/src/BusProNet/Model/CrmAttributesResponse.php index 299bb30..15e526c 100644 --- a/src/BusProNet/Model/CrmAttributesResponse.php +++ b/src/BusProNet/Model/CrmAttributesResponse.php @@ -9,7 +9,7 @@ class CrmAttributesResponse private bool $manager = false; private bool $houseManager = false; private bool $teamer = false; - private ?string $hotelCode = null; + private array $hotelCodes = []; public function getAttributeGroups(): ?array { @@ -70,14 +70,14 @@ class CrmAttributesResponse return $this; } - public function getHotelCode(): ?string + public function getHotelCodes(): array { - return $this->hotelCode; + return $this->hotelCodes; } - public function setHotelCode(?string $hotelCode): static + public function setHotelCodes(array $hotelCodes): static { - $this->hotelCode = $hotelCode; + $this->hotelCodes = $hotelCodes; return $this; } diff --git a/src/BusProNet/ResponseParser.php b/src/BusProNet/ResponseParser.php index 073c6e7..54b6840 100644 --- a/src/BusProNet/ResponseParser.php +++ b/src/BusProNet/ResponseParser.php @@ -142,7 +142,7 @@ class ResponseParser { $groups = []; $isAdmin = $isManager = $isHouseManager = $isTeamer = false; - $hotelCode = null; + $hotelCodes = []; foreach ($xml->selektionsmerkmale->selektionsgruppe as $item) { $group = new CrmAttributeGroup(); @@ -164,7 +164,7 @@ class ResponseParser if (1 === preg_match('/^Hausleitung ([A-Z0-9]+)$/', $attributeLabel, $matches) && true === $attribute->isSelected()) { $isHouseManager = true; - $hotelCode = $matches[1]; + $hotelCodes[] = $matches[1]; } if ($this->config['bpn_crm_id_admin'] === $attribute->getId() && true === $attribute->isSelected()) { $isAdmin = true; @@ -183,7 +183,7 @@ class ResponseParser // Apply additional role and hotel code for testing purposes when provided if ($isAdmin && null !== $this->config['bpn_default_hotel_code']) { $isHouseManager = true; - $hotelCode = $this->config['bpn_default_hotel_code']; + $hotelCodes[] = $this->config['bpn_default_hotel_code']; } $response = new CrmAttributesResponse(); @@ -193,7 +193,7 @@ class ResponseParser ->setManager($isManager) ->setTeamer($isTeamer) ->setHouseManager($isHouseManager) - ->setHotelCode($hotelCode) + ->setHotelCodes($hotelCodes) ; return $response; diff --git a/src/BusProNet/UserDataHandler.php b/src/BusProNet/UserDataHandler.php index f09b639..8148b43 100644 --- a/src/BusProNet/UserDataHandler.php +++ b/src/BusProNet/UserDataHandler.php @@ -55,7 +55,7 @@ class UserDataHandler array $roles, bool $isTeamer = false, array $crmSelections = [], - ?string $hotelCode = null + array $hotelCodes = [] ): User { $user = new User(); $user @@ -64,7 +64,7 @@ class UserDataHandler ->setEmail($profileResponse->getCommunication()->getEmail()) ->setBusProPersonId($profileResponse->getPersonId()) ->setBusProAddressId($profileResponse->getAddressId()) - ->setHotelCode($hotelCode) + ->setHotelCodes($hotelCodes) ->setRoles($roles) ; @@ -95,12 +95,12 @@ class UserDataHandler array $roles, bool $isTeamer = false, array $crmSelections = [], - ?string $hotelCode = null + array $hotelCodes = [] ): void { $user ->setEmail($profileResponse->getCommunication()->getEmail()) ->setRoles($roles) - ->setHotelCode($hotelCode) + ->setHotelCodes($hotelCodes) ; if (true === $isTeamer) { diff --git a/src/Controller/Administrative/Disposition/DocumentUploadController.php b/src/Controller/Administrative/Disposition/DocumentUploadController.php new file mode 100644 index 0000000..ee4ed11 --- /dev/null +++ b/src/Controller/Administrative/Disposition/DocumentUploadController.php @@ -0,0 +1,90 @@ +getTeamer(); + $modalTitle = sprintf('Honorarvertrag für %s hochladen', $teamer); + + $form = $this->createFormBuilder()->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->processUpload($disposition); + + $this->addFlash('success', 'Der Honorarvertrag wurde hochgeladen'); + + return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ + 'uuid' => $disposition->getAssignment()->getUuid(), + ])); + } + + return $this->render('administrative/disposition/modal_document_upload.html.twig', [ + 'disposition' => $disposition, + 'modalTitle' => $modalTitle, + 'form' => $form->createView(), + ]); + } + + private function processUpload(Disposition $disposition): void + { + if (null !== $existingUpload = $disposition->getDocumentByType(Upload::TYPE_CONTRACT)) { + $this->entityManager->remove($existingUpload); + } + + $teamer = $disposition->getTeamer(); + $user = $teamer->getUser(); + + $uploadSession = $this->uploadHandler->getUploadSession(); + + $upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_CONTRACT); + $upload->setStatus(Upload::STATUS_CHECKED); + + $disposition->addDocument($upload); + + $this->entityManager->flush(); + + $this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_CONTRACT, $uploadSession); + $this->uploadHandler->destroyUploadSession(); + + $this->eventDispatcher->dispatch(new DocumentUploadedEvent($upload), DocumentUploadedEvent::NAME); + + $this->logger->info('Document upload for teamer', [ + 'teamer_id' => $teamer->getId(), + 'teamer_name' => $teamer->getFullName(), + 'disposition_id' => $disposition->getId(), + 'document_id' => $upload->getId(), + 'upload_type' => Upload::TYPE_CONTRACT, + ]); + } +} diff --git a/src/Controller/HouseManager/Feedback/ProvideController.php b/src/Controller/Administrative/Feedback/ProvideController.php similarity index 74% rename from src/Controller/HouseManager/Feedback/ProvideController.php rename to src/Controller/Administrative/Feedback/ProvideController.php index 591c92e..afbba83 100644 --- a/src/Controller/HouseManager/Feedback/ProvideController.php +++ b/src/Controller/Administrative/Feedback/ProvideController.php @@ -1,7 +1,8 @@ handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - // Feedbacks without comments are published immediately - if (empty($feedback->getComment())) { + // Feedbacks without comments or provided by admins are published immediately + if ($this->isGranted('ROLE_ADMINISTRATIVE') || empty($feedback->getComment())) { $feedback->setStatus(Feedback::STATUS_PUBLISHED); } @@ -67,10 +70,15 @@ class ProvideController extends AbstractController $this->addFlash('success', 'Das Feedback wurde entgegengenommen'); - return $this->redirectToRoute('app_house_manager_feedback_index'); + $defaultReturnUrlRoute = $this->isGranted('ROLE_HOUSE_MANAGER') ? + 'app_house_manager_feedback_index' : $user->getDefaultRoute(); + + $returnUrl = $this->getReturnUrl($request, $defaultReturnUrlRoute); + + return $this->redirect($returnUrl); } - return $this->render('house_manager/feedback/provide.html.twig', [ + return $this->render('administrative/feedback/provide.html.twig', [ 'form' => $form, 'disposition' => $disposition, ]); diff --git a/src/Controller/HouseManager/Feedback/IndexController.php b/src/Controller/HouseManager/Feedback/IndexController.php index b4169ac..d1b0d79 100644 --- a/src/Controller/HouseManager/Feedback/IndexController.php +++ b/src/Controller/HouseManager/Feedback/IndexController.php @@ -28,7 +28,7 @@ class IndexController extends AbstractController $query = $this ->dispositionRepository - ->getDispositionsWithPendingFeedbackQuery($user->getHotelCode()) + ->getDispositionsWithPendingFeedbackQuery($user->getHotelCodes()) ; $pagination = $this->paginator->paginate( diff --git a/src/Controller/HouseManager/IndexController.php b/src/Controller/HouseManager/IndexController.php index 8bb6733..54a557d 100644 --- a/src/Controller/HouseManager/IndexController.php +++ b/src/Controller/HouseManager/IndexController.php @@ -2,7 +2,6 @@ namespace App\Controller\HouseManager; -use App\BusProNet\DataProvider\HotelDataProvider; use App\Entity\User; use App\Repository\DispositionRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -12,10 +11,8 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; class IndexController extends AbstractController { - public function __construct( - private readonly HotelDataProvider $hotelDataProvider, - private readonly DispositionRepository $dispositionRepository - ) { + public function __construct(private readonly DispositionRepository $dispositionRepository) + { } #[Route('/house-manager', name: 'app_house_manager_index')] @@ -25,19 +22,14 @@ class IndexController extends AbstractController /** @var User $user */ $user = $this->getUser(); - $hotels = $this - ->hotelDataProvider - ->findByCode($user->getHotelCode()) - ; - $newDispositions = $this ->dispositionRepository - ->findNewDispositionsByHotelCode($user->getHotelCode()) + ->findNewDispositionsByHotelCodes($user->getHotelCodes()) ; $pendingFeedbacks = $this ->dispositionRepository - ->findDispositionsWithPendingFeedbackByHotelCode($user->getHotelCode()) + ->findDispositionsWithPendingFeedbackByHotelCodes($user->getHotelCodes()) ; return $this->render('house_manager/index.html.twig', [ diff --git a/src/Controller/Manager/IndexController.php b/src/Controller/Manager/IndexController.php index c9373ef..5e0dbea 100644 --- a/src/Controller/Manager/IndexController.php +++ b/src/Controller/Manager/IndexController.php @@ -25,12 +25,14 @@ class IndexController extends AbstractController $documents = $this->uploadRepository->getNew(); $newDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_NEW); $pendingDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_PENDING); + $overdueFeedbacks = $this->dispositionRepository->findDispositionsWithOverdueFeedback(7); return $this->render('manager/index.html.twig', [ 'dispositions' => $dispositions, 'documents' => $documents, 'newDocumentsCount' => $newDocumentsCount, 'pendingDocumentsCount' => $pendingDocumentsCount, + 'overdueFeedbacks' => $overdueFeedbacks, ]); } } \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php index 6214a04..a9c0295 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -54,6 +54,9 @@ class User implements UserInterface, TimestampableEntityInterface #[ORM\Column(length: 32, nullable: true)] private ?string $hotelCode = null; + #[ORM\Column(type: 'json')] + private array $hotelCodes = []; + #[ORM\Column] private bool $muteNotifications = false; @@ -256,14 +259,14 @@ class User implements UserInterface, TimestampableEntityInterface return $this; } - public function getHotelCode(): ?string + public function getHotelCodes(): array { - return $this->hotelCode; + return $this->hotelCodes; } - public function setHotelCode(?string $hotelCode): static + public function setHotelCodes(array $hotelCodes): static { - $this->hotelCode = $hotelCode; + $this->hotelCodes = $hotelCodes; return $this; } @@ -279,4 +282,18 @@ class User implements UserInterface, TimestampableEntityInterface return $this; } + + public function hasHotelCodeMatch(string $hotelCode): bool + { + foreach ($this->hotelCodes as $userHotelCode) { + if ( + str_starts_with($hotelCode, $userHotelCode) + || str_ends_with($hotelCode, $userHotelCode) + ) { + return true; + } + } + + return false; + } } diff --git a/src/Form/TeamerApplicationType.php b/src/Form/TeamerApplicationType.php index ce7d09f..7964b42 100644 --- a/src/Form/TeamerApplicationType.php +++ b/src/Form/TeamerApplicationType.php @@ -35,11 +35,17 @@ class TeamerApplicationType extends AbstractType } $assignment = $application->getAssignment(); + $pickups = $assignment->getDestination()->getPickups(); + + if (0 === count($pickups)) { + return; + } + $teamer = $application->getTeamer(); if (0 === (int) $assignment->getPickup()) { $pickupChoices = []; - foreach ($assignment->getDestination()->getPickups() as $pickup) { + foreach ($pickups as $pickup) { $pickupChoices[$pickup['city']] = $pickup['busProId']; } ksort($pickupChoices); diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 4951af4..e3bb112 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -244,6 +244,7 @@ class AssignmentRepository extends ServiceEntityRepository ->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer')) ->where($qb->expr()->andX( $qb->expr()->eq('assignment.status', ':status'), + $qb->expr()->neq('assignment.staffingStatus', ':staffingStatus'), $qb->expr()->in('assignment.jobProfile', ':jobProfiles'), $qb->expr()->isNull('application'), $qb->expr()->isNull('disposition'), @@ -253,6 +254,7 @@ class AssignmentRepository extends ServiceEntityRepository ) )) ->setParameter('status', Assignment::STATUS_PUBLISHED) + ->setParameter('staffingStatus', Assignment::STATUS_STAFFED) ->setParameter('jobProfiles', $teamer->getJobProfiles()) ->setParameter('teamer', $teamer) ->setParameter('dateFrom', new \DateTimeImmutable()) diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index 98e0ee2..ffa765b 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -148,8 +148,10 @@ class DispositionRepository extends ServiceEntityRepository ; } - public function findNewDispositionsByHotelCode(string $hotelCode): array + public function findNewDispositionsByHotelCodes(mixed $hotelCode): array { + $hotelCodes = (array) $hotelCode; + $qb = $this->createQueryBuilder('disposition'); return $qb @@ -157,12 +159,15 @@ class DispositionRepository extends ServiceEntityRepository ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( - $qb->expr()->like('destination.hotelCode', ':hotelCode'), + $qb->expr()->orX( + $qb->expr()->in($qb->expr()->substring('destination.hotelCode', 1, 3), ':hotelCodes'), + $qb->expr()->in($qb->expr()->substring('destination.hotelCode', -3, 3), ':hotelCodes'), + ), $qb->expr()->gte('destination.dateTo', ':dateTo'), $qb->expr()->eq('disposition.status', ':status') )) ->orderBy('destination.dateFrom', 'ASC') - ->setParameter('hotelCode', '%'.$hotelCode.'%') + ->setParameter('hotelCodes', $hotelCodes) ->setParameter('dateTo', new \DateTimeImmutable()) ->setParameter('status', Disposition::STATUS_CONFIRMED) ->getQuery() @@ -170,8 +175,10 @@ class DispositionRepository extends ServiceEntityRepository ; } - public function getDispositionsWithPendingFeedbackQuery(?string $hotelCode = null, ?int $offsetDays = null): Query + public function getDispositionsWithPendingFeedbackQuery(mixed $hotelCode = null, ?int $offsetDays = null): Query { + $hotelCodes = (array) $hotelCode; + $qb = $this->createQueryBuilder('disposition'); $qb @@ -201,8 +208,11 @@ class DispositionRepository extends ServiceEntityRepository if (null !== $hotelCode) { $qb - ->andWhere($qb->expr()->like('destination.hotelCode', ':hotelCode')) - ->setParameter('hotelCode', '%'.$hotelCode.'%') + ->andWhere($qb->expr()->orX( + $qb->expr()->in($qb->expr()->substring('destination.hotelCode', 1, 3), ':hotelCodes'), + $qb->expr()->in($qb->expr()->substring('destination.hotelCode', -3, 3), ':hotelCodes'), + )) + ->setParameter('hotelCodes', $hotelCodes) ; } @@ -217,7 +227,7 @@ class DispositionRepository extends ServiceEntityRepository ; } - public function findDispositionsWithPendingFeedbackByHotelCode(string $hotelCode): array + public function findDispositionsWithPendingFeedbackByHotelCodes(mixed $hotelCode): array { return $this ->getDispositionsWithPendingFeedbackQuery($hotelCode) diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 479e310..b925f8b 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -32,16 +32,14 @@ class UserRepository extends ServiceEntityRepository $qb = $this->createQueryBuilder('u'); return $qb - ->where($qb->expr()->orX( - $qb->expr()->like('u.roles', ':role_admin'), - $qb->expr()->like('u.roles', ':role_manager'), - $qb->expr()->like('u.roles', ':role_house_manager') - )) + ->where("JSON_CONTAINS(u.roles, :role_admin) = 1") + ->orWhere("JSON_CONTAINS(u.roles, :role_manager) = 1") + ->orWhere("JSON_CONTAINS(u.roles, :role_house_manager) = 1") ->orderBy('u.lastName', 'ASC') ->setParameters([ - 'role_admin' => '%"ROLE_ADMIN"%', - 'role_manager' => '%"ROLE_MANAGER"%', - 'role_house_manager' => '%"ROLE_HOUSE_MANAGER"%', + 'role_admin' => json_encode('ROLE_ADMIN'), + 'role_manager' => json_encode('ROLE_MANAGER'), + 'role_house_manager' => json_encode('ROLE_HOUSE_MANAGER'), ]) ->getQuery() ->getResult() @@ -50,22 +48,29 @@ class UserRepository extends ServiceEntityRepository /** * @param string $role - * @param string $hotelCode + * @param string[]|string $hotelCode * @return User[] */ - public function getUsersByRoleAndHotelCode(string $role, string $hotelCode): array + public function getUsersByRoleAndHotelCode(string $role, mixed $hotelCode): array { - $hotelCodeBase = substr($hotelCode, 0, 3); + $hotelCodes = (array) $hotelCode; + + $hotelCodesBase = array_map(function ($code) { + return substr($code, 0, 3); + }, $hotelCodes); $qb = $this->createQueryBuilder('user'); + foreach ($hotelCodesBase as $index => $code) { + $qb + ->orWhere('JSON_CONTAINS(user.hotelCodes, :hotelCode_'.$index.') = 1') + ->setParameter('hotelCode_'.$index, json_encode($code)) + ; + } + return $qb - ->where($qb->expr()->andX( - $qb->expr()->eq('user.hotelCode', ':hotelCode'), - $qb->expr()->like('user.roles', ':role') - )) - ->setParameter('hotelCode', $hotelCodeBase) - ->setParameter('role', '%"'.$role.'"%') + ->andWhere('JSON_CONTAINS(user.roles, :role) = 1') + ->setParameter('role', json_encode($role)) ->getQuery() ->getResult() ; @@ -76,17 +81,15 @@ class UserRepository extends ServiceEntityRepository $qb = $this->createQueryBuilder('user'); $users = $qb - ->where($qb->expr()->andX( - $qb->expr()->orX( - $qb->expr()->like('user.lastName', ':search'), - $qb->expr()->like('user.firstName', ':search'), - )), - $qb->expr()->like('user.roles', ':role') - ) + ->where($qb->expr()->orX( + $qb->expr()->like('user.lastName', ':search'), + $qb->expr()->like('user.firstName', ':search'), + )) + ->andWhere('JSON_CONTAINS(user.roles, :role) = 1') ->orderBy('user.lastName', 'ASC') ->addOrderBy('user.firstName', 'ASC') ->setParameter('search', '%'.$this->escapeLikeWildcards($search).'%') - ->setParameter('role', '%"'.$this->escapeLikeWildcards($role).'"%') + ->setParameter('role', json_encode($role)) ->getQuery() ->getResult() ; diff --git a/src/Security/BpnAuthenticator.php b/src/Security/BpnAuthenticator.php index 21f2a0e..a741e27 100644 --- a/src/Security/BpnAuthenticator.php +++ b/src/Security/BpnAuthenticator.php @@ -137,7 +137,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent if (null !== $user) { $this ->userDataHandler - ->updateLocalUser($user, $profileResponse, $roles, $isTeamer, $crmSelections, $crmAttributes->getHotelCode()) + ->updateLocalUser($user, $profileResponse, $roles, $isTeamer, $crmSelections, $crmAttributes->getHotelCodes()) ; return $user; @@ -145,7 +145,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent return $this ->userDataHandler - ->createLocalUser($profileResponse, $roles, $isTeamer, $crmSelections, $crmAttributes->getHotelCode()) + ->createLocalUser($profileResponse, $roles, $isTeamer, $crmSelections, $crmAttributes->getHotelCodes()) ; } } diff --git a/src/Security/Voter/DispositionVoter.php b/src/Security/Voter/DispositionVoter.php index bf269f8..9d55788 100644 --- a/src/Security/Voter/DispositionVoter.php +++ b/src/Security/Voter/DispositionVoter.php @@ -2,9 +2,8 @@ namespace App\Security\Voter; -use App\BusProNet\DataProvider\HotelDataProvider; -use App\BusProNet\Model\Hotel; use App\Entity\Disposition; +use App\Entity\User; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -18,10 +17,8 @@ class DispositionVoter extends Voter public const INVOICE = 'INVOICE'; public const FEEDBACK = 'FEEDBACK'; - public function __construct( - private readonly Security $security, - private readonly HotelDataProvider $hotelDataProvider - ) { + public function __construct(private readonly Security $security) + { } protected function supports(string $attribute, mixed $subject): bool @@ -46,9 +43,10 @@ class DispositionVoter extends Voter $disposition = $subject; return match ($attribute) { - static::VIEW, static::EDIT, static::CONTRACT, static::INVOICE => $this->assertAdministrativeAccess() || $this->assertTeamerAccess($disposition), + static::VIEW, static::EDIT, static::CONTRACT, static::INVOICE => $this->assertAdministrativeAccess() + || $this->assertTeamerAccess($token, $disposition), static::DELETE => $this->assertAdminAccess(), - static::FEEDBACK => $this->assertHouseManagerAccess($token, $disposition), + static::FEEDBACK => $this->assertAdminAccess() || $this->assertHouseManagerAccess($token, $disposition), default => false, }; } @@ -69,28 +67,29 @@ class DispositionVoter extends Voter return false; } - $hotels = $this - ->hotelDataProvider - ->findByCode($token->getUser()->getHotelCode()) - ; - $hotelBusProIds = array_map(function (Hotel $hotel) { - return $hotel->getBusProId(); - }, $hotels); - $destination = $disposition - ->getAssignment() - ->getDestination() - ; + /** @var User $user */ + $user = $token->getUser(); - return in_array($destination->getHotelBusProId(), $hotelBusProIds) - && $destination->getDateTo() < new \DateTimeImmutable(); + $destination = $disposition + ->getAssignment() + ->getDestination() + ; + + $isMatchingHotel = $user->hasHotelCodeMatch($destination->getHotelCode()); + $isPast = $destination->getDateTo() < new \DateTimeImmutable(); + + return $isMatchingHotel && $isPast; } - private function assertTeamerAccess(Disposition $disposition): bool + private function assertTeamerAccess(TokenInterface $token, Disposition $disposition): bool { if (false === $this->security->isGranted('ROLE_TEAMER')) { return false; } - return $this->security->getUser()->getTeamer() === $disposition->getTeamer(); + /** @var User $user */ + $user = $token->getUser(); + + return $user->getTeamer() === $disposition->getTeamer(); } -} \ No newline at end of file +} diff --git a/src/Service/Cron/FeedbackReminderService.php b/src/Service/Cron/FeedbackReminderService.php index cfa815f..e0ddd26 100644 --- a/src/Service/Cron/FeedbackReminderService.php +++ b/src/Service/Cron/FeedbackReminderService.php @@ -38,24 +38,30 @@ class FeedbackReminderService continue; } - // Get hotel base code to find hotel manager later - $hotelBaseCode = substr($hotel->getCode(), 0, 3); + // Get hotel code to find hotel manager later + $hotelCode = $hotel->getCode(); - if (false === array_key_exists($hotelBaseCode, $sortedFeedbacks)) { - $sortedFeedbacks[$hotelBaseCode] = []; + if (false === array_key_exists($hotelCode, $sortedFeedbacks)) { + $sortedFeedbacks[$hotelCode] = []; } // Sort feedback by hotel base code - $sortedFeedbacks[$hotelBaseCode][] = $disposition; + $sortedFeedbacks[$hotelCode][] = $disposition; } - // Determine hotel managers to notify - $hotelBaseCodes = array_keys($sortedFeedbacks); + // find hotel managers by hotel codes starting or ending with base code + $hotelCodes = array_keys($sortedFeedbacks); + $hotelBaseCodes = []; + foreach ($hotelCodes as $hotelCode) { + $hotelBaseCodes[] = substr($hotelCode, 0, 3); + $hotelBaseCodes[] = substr($hotelCode, -3, 3); + } /** @var User[] $hotelManagers */ - $hotelManagers = $this->userRepository->findBy([ - 'hotelCode' => $hotelBaseCodes, - ]); + $hotelManagers = $this + ->userRepository + ->getUsersByRoleAndHotelCode('ROLE_HOTEL_MANAGER', $hotelBaseCodes) + ; if (0 === count($hotelManagers)) { return 'No feedback reminders to be sent to hotel managers'; @@ -65,8 +71,14 @@ class FeedbackReminderService if (true === $manager->isMuteNotifications()) { continue; } + $feedbackKeys = array_filter(array_keys($sortedFeedbacks), function (string $key) use ($manager) { + return $manager->hasHotelCodeMatch($key); + }); + $feedbacksForManager = array_filter($sortedFeedbacks, function ($key) use ($feedbackKeys) { + return in_array($key, $feedbackKeys); + }, ARRAY_FILTER_USE_KEY); $this->mailer->createAndSendEmail([ - 'feedbacks' => $sortedFeedbacks[$manager->getHotelCode()], + 'feedbacks' => $feedbacksForManager, ], [ 'to' => $manager->getEmail(), 'subject' => 'Reminder: Offene Feedbacks', diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index d693332..d8ec903 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -166,7 +166,7 @@ {% for disposition in overdueFeedbacks %} {% set assignment = disposition.assignment %}
  • - {{ icon('feedback', 'w-4 h-4 shrink-0') }} {{ disposition.teamer }} diff --git a/templates/administrative/assignment/detail.html.twig b/templates/administrative/assignment/detail.html.twig index b95deb4..abe5b72 100644 --- a/templates/administrative/assignment/detail.html.twig +++ b/templates/administrative/assignment/detail.html.twig @@ -179,6 +179,15 @@
    + {% if is_granted('CONTRACT_SUPPLEMENTARY', disposition) %} + + {% endif %} {% if is_granted('DELETE', disposition) %}
    + + {{ form_rest(form) }} + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/templates/house_manager/feedback/provide.html.twig b/templates/administrative/feedback/provide.html.twig similarity index 98% rename from templates/house_manager/feedback/provide.html.twig rename to templates/administrative/feedback/provide.html.twig index a6273a2..63bd6c3 100644 --- a/templates/house_manager/feedback/provide.html.twig +++ b/templates/administrative/feedback/provide.html.twig @@ -1,4 +1,4 @@ -{% extends 'house_manager/layout.html.twig' %} +{% extends 'administrative/layout.html.twig' %} {% block title %}Feedback abgeben{% endblock %} diff --git a/templates/administrative/layout.html.twig b/templates/administrative/layout.html.twig index 59f774f..990b647 100644 --- a/templates/administrative/layout.html.twig +++ b/templates/administrative/layout.html.twig @@ -3,6 +3,8 @@ {% block main_menu %} {% if is_granted('ROLE_MANAGER') %} {{ knp_menu_render(knp_menu_get('manager_main')) }} + {% elseif is_granted('ROLE_HOUSE_MANAGER') %} + {{ knp_menu_render(knp_menu_get('house_manager_main')) }} {% elseif is_granted('ROLE_ADMIN') %} {{ knp_menu_render(knp_menu_get('admin_main')) }} {% endif %} @@ -11,6 +13,8 @@ {% block mobile_menu %} {% if is_granted('ROLE_MANAGER') %} {{ knp_menu_render(knp_menu_get('manager_main')) }} + {% elseif is_granted('ROLE_HOUSE_MANAGER') %} + {{ knp_menu_render(knp_menu_get('house_manager_main')) }} {% elseif is_granted('ROLE_ADMIN') %} {{ knp_menu_render(knp_menu_get('admin_main')) }} {% endif %} diff --git a/templates/house_manager/feedback/index.html.twig b/templates/house_manager/feedback/index.html.twig index ad14bfb..627590e 100644 --- a/templates/house_manager/feedback/index.html.twig +++ b/templates/house_manager/feedback/index.html.twig @@ -1,4 +1,4 @@ -{% extends 'house_manager/layout.html.twig' %} +{% extends 'administrative/layout.html.twig' %} {% block title %}Ausstehendes Feedback{% endblock %} diff --git a/templates/house_manager/index.html.twig b/templates/house_manager/index.html.twig index 3e866e9..2e9739e 100644 --- a/templates/house_manager/index.html.twig +++ b/templates/house_manager/index.html.twig @@ -1,4 +1,4 @@ -{% extends 'house_manager/layout.html.twig' %} +{% extends 'administrative/layout.html.twig' %} {% macro dispositionData(disposition) %} {% set assignment = disposition.assignment %} @@ -49,7 +49,7 @@ {% for disposition in pendingFeedbacks %} {% set assignment = disposition.assignment %}
  • - {{ icon('feedback', 'w-4 h-4 shrink-0') }} {{ _self.dispositionData(disposition) }} diff --git a/templates/house_manager/layout.html.twig b/templates/house_manager/layout.html.twig deleted file mode 100644 index 23b1533..0000000 --- a/templates/house_manager/layout.html.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'layout.html.twig' %} - -{% block main_menu %} - {{ knp_menu_render(knp_menu_get('house_manager_main')) }} -{% endblock %} - -{% block mobile_menu %} - {{ knp_menu_render(knp_menu_get('house_manager_main')) }} -{% endblock %} diff --git a/templates/manager/index.html.twig b/templates/manager/index.html.twig index da768d2..58b3c77 100644 --- a/templates/manager/index.html.twig +++ b/templates/manager/index.html.twig @@ -1,4 +1,4 @@ -{% extends 'manager/layout.html.twig' %} +{% extends 'administrative/layout.html.twig' %} {% block content %}
    @@ -53,5 +53,34 @@ {% endfor %}
    +
    +

    + überfällige Feedbacks +

    +
    +
    {% endblock %} diff --git a/templates/manager/layout.html.twig b/templates/manager/layout.html.twig deleted file mode 100644 index 039e670..0000000 --- a/templates/manager/layout.html.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'layout.html.twig' %} - -{% block main_menu %} - {{ knp_menu_render(knp_menu_get('manager_main')) }} -{% endblock %} - -{% block mobile_menu %} - {{ knp_menu_render(knp_menu_get('manager_main')) }} -{% endblock %} diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php new file mode 100644 index 0000000..50f9096 --- /dev/null +++ b/tests/Entity/UserTest.php @@ -0,0 +1,26 @@ +setHotelCodes(['ABC', 'DEF', 'GHI']); + + $isMatch = $user->hasHotelCodeMatch('ABCXXX'); + $this->assertTrue($isMatch); + + $isMatch = $user->hasHotelCodeMatch('XXXGHI'); + $this->assertTrue($isMatch); + + $isMatch = $user->hasHotelCodeMatch('XXDEFXX'); + $this->assertFalse($isMatch); + } +}