WIP: Implement role house manager and feedback providing

This commit is contained in:
Björn Fromme
2023-11-04 18:24:54 +01:00
parent 538d39f63e
commit d5da0d10d5
23 changed files with 576 additions and 37 deletions
@@ -37,14 +37,16 @@ class HotelDataProvider
return $this->getAll()[$busProId] ?? null;
}
public function findByCode(string $code): ?Hotel
public function findByCode(string $code): array
{
$hotels = [];
foreach ($this->getAll() as $hotel) {
if ($code === $hotel->getCode()) {
return $hotel;
if (str_starts_with($hotel->getCode(), $code)) {
$hotels[] = $hotel;
}
}
return null;
return $hotels;
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Controller\HouseManager\Feedback;
use App\BusProNet\DataProvider\HotelDataProvider;
use App\BusProNet\Model\Hotel;
use App\Entity\User;
use App\Repository\DispositionRepository;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(
private readonly DispositionRepository $dispositionRepository,
private readonly HotelDataProvider $hotelDataProvider,
private readonly PaginatorInterface $paginator
) {
}
#[Route('/house-manager/pending-feedback', name: 'app_house_manager_feedback_index')]
#[IsGranted('ROLE_HOUSE_MANAGER')]
public function index(Request $request): Response
{
/** @var User $user */
$user = $this->getUser();
$hotels = $this
->hotelDataProvider
->findByCode($user->getHotelCode())
;
$hotelBusProIds = array_map(function (Hotel $hotel) {
return $hotel->getBusProId();
}, $hotels);
$query = $this
->dispositionRepository
->getPendingFeedbackQuery($hotelBusProIds)
;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
'defaultSortFieldName' => 'assignment.dateFrom',
'defaultSortDirection' => 'asc',
]
);
return $this->render('house_manager/feedback/index.html.twig', [
'pagination' => $pagination,
]);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Controller\HouseManager\Feedback;
use App\Entity\Disposition;
use App\Entity\Feedback;
use App\Event\FeedbackProvidedEvent;
use App\Form\FeedbackType;
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\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class ProvideController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly LoggerInterface $logger
) {
}
#[Route('/house-manager/feedback/provide/{uuid}', name: 'app_house_manager_feedback_provide')]
#[IsGranted('FEEDBACK', subject: 'disposition')]
public function index(Disposition $disposition, Request $request): Response
{
$assignment = $disposition->getAssignment();
$destination = $assignment->getDestination();
$feebackSet = $assignment
->getJobProfile()
->getFeedbackSet()
;
$feedback = new Feedback();
$form = $this->createForm(FeedbackType::class, $feedback, ['feedback_set' => $feebackSet]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$feedback
->setAssignmentDestination($destination)
->setAssignmentDate($assignment->getEffectivePeriod()->start->toDateTimeImmutable())
;
$teamer = $disposition->getTeamer();
$teamer->addFeedback($feedback);
$disposition->setFeedback($feedback);
$this->entityManager->persist($feedback);
$this->entityManager->flush();
$this->eventDispatcher->dispatch(new FeedbackProvidedEvent($feedback), FeedbackProvidedEvent::NAME);
$this->logger->info('Feedback provided', [
'feedback_id' => $feedback->getId(),
'teamer' => (string) $teamer,
'destination' => (string) $destination,
]);
$this->addFlash('success', 'Das Feedback wurde entgegengenommen');
return $this->redirectToRoute('app_house_manager_feedback_index');
}
return $this->render('house_manager/feedback/provide.html.twig', [
'form' => $form,
'disposition' => $disposition,
]);
}
}
@@ -11,6 +11,6 @@ class IndexController extends AbstractController
#[Route('/house-manager', name: 'app_house_manager_index')]
public function index(): Response
{
return $this->render('');
return $this->render('house_manager/index.html.twig');
}
}
+1
View File
@@ -49,6 +49,7 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
private Collection $documents;
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Feedback $feedback = null;
public function __construct(Application $application)
+30
View File
@@ -35,6 +35,12 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface
#[ORM\Column]
private array $ratings = [];
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $comment = null;
#[ORM\Column]
private ?bool $commentPublic = false;
public function __construct()
{
$this->uuid = Uuid::v4();
@@ -97,4 +103,28 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface
return $this;
}
public function getComment(): ?string
{
return $this->comment;
}
public function setComment(?string $comment): static
{
$this->comment = $comment;
return $this;
}
public function isCommentPublic(): ?bool
{
return $this->commentPublic;
}
public function setCommentPublic(bool $commentPublic): static
{
$this->commentPublic = $commentPublic;
return $this;
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Event;
use App\Entity\Feedback;
use Symfony\Contracts\EventDispatcher\Event;
class FeedbackProvidedEvent extends Event
{
public const NAME = 'feedback.provided';
public function __construct(private readonly Feedback $feedback)
{
}
public function getFeedback(): Feedback
{
return $this->feedback;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Form;
use App\Entity\FeedbackSet;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FeedbackRatingsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$ratings = $options['feedback_set']->getRatings();
foreach ($ratings as $index => $rating) {
$builder->add('rating_'.$index, ChoiceType::class, [
'label' => $rating,
'choices' => array_combine(range(1, 6), range(1,6)),
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired(['feedback_set'])
->setAllowedTypes('feedback_set', FeedbackSet::class)
;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Form;
use App\Entity\Feedback;
use App\Entity\FeedbackSet;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class FeedbackType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('ratings', FeedbackRatingsType::class, [
'label' => false,
'feedback_set' => $options['feedback_set'],
])
->add('comment', TextareaType::class, [
'label' => 'Kommentar',
'required' => false,
'attr' => [
'rows' => 5,
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => Feedback::class,
])
->setRequired(['feedback_set'])
->setAllowedTypes('feedback_set', FeedbackSet::class)
;
}
}
+11
View File
@@ -99,6 +99,17 @@ abstract class AbstractMenuBuilder
}
}
protected function addHouseManagerItem(ItemInterface $menu): void
{
if ($this->security->isGranted('ROLE_HOUSE_MANAGER')) {
$menu
->addChild('zum Hausmanagerbereich', ['route' => 'app_house_manager_index'])
->setChildrenAttribute('title', 'zum Hausmanagerbereich')
->setExtra('icon', 'user')
;
}
}
protected function addLogoutItem(ItemInterface $menu): void
{
$token = $this->security->getToken();
+5 -1
View File
@@ -39,7 +39,7 @@ class AdminMenuBuilder extends AbstractMenuBuilder
],
[
'route' => 'app_admin_teamer_index',
'title' => 'Teamer:innenübersicht',
'title' => 'Teamübersicht',
'icon' => 'users',
'hideChildren' => true,
'children' => $this->getTeamerMenuItems(),
@@ -165,6 +165,10 @@ class AdminMenuBuilder extends AbstractMenuBuilder
$this->addDivider($menu);
$this->addHouseManagerItem($menu);
$this->addDivider($menu);
$this->addLogoutItem($menu);
return $menu;
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Menu;
use Knp\Menu\ItemInterface;
class HouseManagerMenuBuilder extends AbstractMenuBuilder
{
public function createMainMenu(array $options): ItemInterface
{
$menuItems = [
[
'route' => 'app_house_manager_index',
'title' => 'Dashboard',
'icon' => 'chart',
],
[
'route' => 'app_house_manager_feedback_index',
'title' => 'Feedback',
'icon' => 'feedback',
'hideChildren' => true,
'children' => [
[
'route' => 'app_house_manager_feedback_provide',
'title' => 'Feedback abgeben',
'routeParameters' => $this->getDefaultRouteParameters('uuid'),
]
],
],
];
$menu = $this->createMenu($menuItems);
$this->addDivider($menu);
$this->addAdminItem($menu);
$this->addDivider($menu);
$this->addLogoutItem($menu);
return $menu;
}
}
+25
View File
@@ -109,4 +109,29 @@ class DispositionRepository extends ServiceEntityRepository
->getResult()
;
}
public function getPendingFeedbackQuery(array $hotelBusProIds = null): Query
{
$qb = $this->createQueryBuilder('disposition');
$qb
->select('disposition', 'assignment', 'destination')
->innerJoin('disposition.assignment', 'assignment')
->innerJoin('assignment.destination', 'destination')
->where($qb->expr()->andX(
$qb->expr()->isNull('disposition.feedback'),
$qb->expr()->lt('destination.dateTo', ':dateTo')
))
->setParameter('dateTo', new \DateTimeImmutable())
;
if (null !== $hotelBusProIds) {
$qb
->andWhere($qb->expr()->in('destination.hotelBusProId', ':hotelBusProIds'))
->setParameter('hotelBusProIds', $hotelBusProIds)
;
}
return $qb->getQuery();
}
}
-25
View File
@@ -38,29 +38,4 @@ class FeedbackRepository extends ServiceEntityRepository
$this->getEntityManager()->flush();
}
}
// /**
// * @return Feedback[] Returns an array of Feedback objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('f.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Feedback
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+31 -4
View File
@@ -2,6 +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\Component\Security\Core\Authentication\Token\TokenInterface;
@@ -14,6 +16,11 @@ class DispositionVoter extends Voter
public const DELETE = 'DELETE';
public const CONTRACT = 'CONTRACT';
public const INVOICE = 'INVOICE';
public const FEEDBACK = 'FEEDBACK';
public function __construct(private readonly HotelDataProvider $hotelDataProvider)
{
}
protected function supports(string $attribute, mixed $subject): bool
{
@@ -26,18 +33,38 @@ class DispositionVoter extends Voter
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
/** @var Disposition $disposition */
$disposition = $subject;
/** @var User $user */
$user = $token->getUser();
// Administrative users have full access to all dispositions
if (in_array('ROLE_ADMINISTRATIVE', $token->getRoleNames())) {
return true;
}
// House managers may provide feedback for dispositions that are past
// and are associated with their hotel
if (in_array('ROLE_HOUSE_MANAGER', $token->getRoleNames()) && static::FEEDBACK === $attribute) {
$hotels = $this
->hotelDataProvider
->findByCode($user->getHotelCode())
;
$hotelBusProIds = array_map(function (Hotel $hotel) {
return $hotel->getBusProId();
}, $hotels);
$destination = $disposition
->getAssignment()
->getDestination()
;
return in_array($destination->getHotelBusProId(), $hotelBusProIds)
&& $destination->getDateTo() < new \DateTimeImmutable();
}
// Teamers may only view or edit their own dispositions
if (in_array('ROLE_TEAMER', $token->getRoleNames())) {
/** @var User $user */
$user = $token->getUser();
$teamer = $user->getTeamer();
/** @var Disposition $disposition */
$disposition = $subject;
switch ($attribute) {
case static::VIEW: