Merge branch 'feature/multiple-hotelcodes' into develop
This commit is contained in:
+5
-1
@@ -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!
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+73
-1
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
+14
@@ -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',
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250107163840 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\Disposition;
|
||||
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\DocumentUploadedEvent;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
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;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class DocumentUploadController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UploadHandler $uploadHandler,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/disposition/document-upload/{uuid}',
|
||||
name: 'app_administrative_disposition_document_upload'
|
||||
)]
|
||||
#[IsGranted('CONTRACT_SUPPLEMENTARY', subject: 'disposition')]
|
||||
public function index(Disposition $disposition, Request $request): Response
|
||||
{
|
||||
$teamer = $disposition->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+14
-6
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\HouseManager\Feedback;
|
||||
namespace App\Controller\Administrative\Feedback;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Feedback;
|
||||
use App\Entity\User;
|
||||
@@ -17,13 +18,15 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ProvideController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/house-manager/feedback/provide/{uuid}', name: 'app_house_manager_feedback_provide')]
|
||||
#[Route('/administrative/feedback/provide/{uuid}', name: 'app_administrative_feedback_provide')]
|
||||
#[IsGranted('FEEDBACK', subject: 'disposition')]
|
||||
public function index(Disposition $disposition, Request $request): Response
|
||||
{
|
||||
@@ -48,8 +51,8 @@ class ProvideController extends AbstractController
|
||||
$form->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,
|
||||
]);
|
||||
@@ -28,7 +28,7 @@ class IndexController extends AbstractController
|
||||
|
||||
$query = $this
|
||||
->dispositionRepository
|
||||
->getDispositionsWithPendingFeedbackQuery($user->getHotelCode())
|
||||
->getDispositionsWithPendingFeedbackQuery($user->getHotelCodes())
|
||||
;
|
||||
|
||||
$pagination = $this->paginator->paginate(
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+21
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
;
|
||||
|
||||
@@ -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())
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
{% for disposition in overdueFeedbacks %}
|
||||
{% set assignment = disposition.assignment %}
|
||||
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden">
|
||||
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
|
||||
<a href="{{ path('app_administrative_feedback_provide', { 'uuid': disposition.uuid, 'r': return_url() }) }}"
|
||||
class="flex items-center space-x-1">
|
||||
{{ icon('feedback', 'w-4 h-4 shrink-0') }}
|
||||
<span class="whitespace-nowrap">{{ disposition.teamer }}</span>
|
||||
|
||||
@@ -179,6 +179,15 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
{% if is_granted('CONTRACT_SUPPLEMENTARY', disposition) %}
|
||||
<button type="button"
|
||||
title="Dokumentenupload"
|
||||
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ icon('upload') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_granted('DELETE', disposition) %}
|
||||
<button type="button"
|
||||
class="text-red-500"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}{{ modalTitle }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }}) }}
|
||||
<div class="pb-4">
|
||||
{% include '_partials/_upload_collection_form.html.twig' with {
|
||||
'endpoint_upload': path('_uploader_upload_contract'),
|
||||
'max_filesize': 5,
|
||||
'max_files': 1,
|
||||
'accepted_files': 'image/jpg,image/jpeg,application/pdf',
|
||||
} %}
|
||||
</div>
|
||||
<button type="submit" class="btn">
|
||||
Upload speichern
|
||||
</button>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{% extends 'house_manager/layout.html.twig' %}
|
||||
{% extends 'administrative/layout.html.twig' %}
|
||||
|
||||
{% block title %}Feedback abgeben{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'house_manager/layout.html.twig' %}
|
||||
{% extends 'administrative/layout.html.twig' %}
|
||||
|
||||
{% block title %}Ausstehendes Feedback{% endblock %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden">
|
||||
<a href="{{ path('app_house_manager_feedback_provide', { 'uuid': disposition.uuid, 'r': return_url() }) }}"
|
||||
<a href="{{ path('app_administrative_feedback_provide', { 'uuid': disposition.uuid, 'r': return_url() }) }}"
|
||||
class="flex items-center space-x-1">
|
||||
{{ icon('feedback', 'w-4 h-4 shrink-0') }}
|
||||
{{ _self.dispositionData(disposition) }}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends 'manager/layout.html.twig' %}
|
||||
{% extends 'administrative/layout.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
@@ -53,5 +53,34 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-gray-100 border border-gray-200 rounded-md px-4 py-2">
|
||||
<h2 class="font-bold text-lg pb-2">
|
||||
überfällige Feedbacks
|
||||
</h2>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for disposition in overdueFeedbacks %}
|
||||
{% set assignment = disposition.assignment %}
|
||||
<li class="py-2 first:pt-0 last:pb-0 overflow-x-hidden">
|
||||
<a href="{{ path('app_administrative_feedback_provide', { 'uuid': disposition.uuid, 'r': return_url() }) }}"
|
||||
class="flex items-center space-x-1">
|
||||
{{ icon('feedback', 'w-4 h-4 shrink-0') }}
|
||||
<span class="whitespace-nowrap">{{ disposition.teamer }}</span>
|
||||
{% if assignment.destination %}
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination }}</span>
|
||||
{% endif %}
|
||||
{% if assignment.jobProfile %}
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.jobProfile.name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="py-2 first:pt-0 last:pb-0">
|
||||
-
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
public function testHotelCodeMatching(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user