From d5da0d10d538321c2b3273d6d51320fe88fafd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Sat, 4 Nov 2023 18:24:54 +0100 Subject: [PATCH] WIP: Implement role house manager and feedback providing --- assets/images/icons.svg | 3 + config/services.yaml | 8 ++ migrations/Version20231104170014.php | 35 ++++++ .../DataProvider/HotelDataProvider.php | 10 +- .../HouseManager/Feedback/IndexController.php | 58 ++++++++++ .../Feedback/ProvideController.php | 71 ++++++++++++ .../HouseManager/IndexController.php | 2 +- src/Entity/Disposition.php | 1 + src/Entity/Feedback.php | 30 +++++ src/Event/FeedbackProvidedEvent.php | 20 ++++ src/Form/FeedbackRatingsType.php | 32 ++++++ src/Form/FeedbackType.php | 41 +++++++ src/Menu/AbstractMenuBuilder.php | 11 ++ src/Menu/AdminMenuBuilder.php | 6 +- src/Menu/HouseManagerMenuBuilder.php | 44 +++++++ src/Repository/DispositionRepository.php | 25 ++++ src/Repository/FeedbackRepository.php | 25 ---- src/Security/Voter/DispositionVoter.php | 35 +++++- templates/admin/teamer/index.html.twig | 4 +- .../house_manager/feedback/index.html.twig | 108 ++++++++++++++++++ .../house_manager/feedback/provide.html.twig | 31 +++++ templates/house_manager/index.html.twig | 4 + templates/house_manager/layout.html.twig | 9 ++ 23 files changed, 576 insertions(+), 37 deletions(-) create mode 100644 migrations/Version20231104170014.php create mode 100644 src/Controller/HouseManager/Feedback/IndexController.php create mode 100644 src/Controller/HouseManager/Feedback/ProvideController.php create mode 100644 src/Event/FeedbackProvidedEvent.php create mode 100644 src/Form/FeedbackRatingsType.php create mode 100644 src/Form/FeedbackType.php create mode 100644 src/Menu/HouseManagerMenuBuilder.php create mode 100644 templates/house_manager/feedback/index.html.twig create mode 100644 templates/house_manager/feedback/provide.html.twig create mode 100644 templates/house_manager/index.html.twig create mode 100644 templates/house_manager/layout.html.twig diff --git a/assets/images/icons.svg b/assets/images/icons.svg index 46a5ff5..eb8e636 100644 --- a/assets/images/icons.svg +++ b/assets/images/icons.svg @@ -1,4 +1,7 @@ + + + diff --git a/config/services.yaml b/config/services.yaml index eb40662..06dccf8 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -85,6 +85,14 @@ services: method: createMainMenu alias: teamer_main + App\Menu\HouseManagerMenuBuilder: + arguments: + $factory: '@knp_menu.factory' + tags: + - name: knp_menu.menu_builder + method: createMainMenu + alias: house_manager_main + App\Service\Upload\UploadHandler: arguments: $orphanageManager: '@oneup_uploader.orphanage_manager' diff --git a/migrations/Version20231104170014.php b/migrations/Version20231104170014.php new file mode 100644 index 0000000..8b5b568 --- /dev/null +++ b/migrations/Version20231104170014.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE disposition DROP FOREIGN KEY FK_4C58BF60D249A887'); + $this->addSql('ALTER TABLE disposition ADD CONSTRAINT FK_4C58BF60D249A887 FOREIGN KEY (feedback_id) REFERENCES feedback (id) ON DELETE SET NULL'); + $this->addSql('ALTER TABLE feedback ADD comment LONGTEXT DEFAULT NULL, ADD comment_public TINYINT(1) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE disposition DROP FOREIGN KEY FK_4C58BF60D249A887'); + $this->addSql('ALTER TABLE disposition ADD CONSTRAINT FK_4C58BF60D249A887 FOREIGN KEY (feedback_id) REFERENCES feedback (id)'); + $this->addSql('ALTER TABLE feedback DROP comment, DROP comment_public'); + } +} diff --git a/src/BusProNet/DataProvider/HotelDataProvider.php b/src/BusProNet/DataProvider/HotelDataProvider.php index 4fea1e8..8807605 100644 --- a/src/BusProNet/DataProvider/HotelDataProvider.php +++ b/src/BusProNet/DataProvider/HotelDataProvider.php @@ -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; } } \ No newline at end of file diff --git a/src/Controller/HouseManager/Feedback/IndexController.php b/src/Controller/HouseManager/Feedback/IndexController.php new file mode 100644 index 0000000..010872b --- /dev/null +++ b/src/Controller/HouseManager/Feedback/IndexController.php @@ -0,0 +1,58 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/src/Controller/HouseManager/Feedback/ProvideController.php b/src/Controller/HouseManager/Feedback/ProvideController.php new file mode 100644 index 0000000..f5e1697 --- /dev/null +++ b/src/Controller/HouseManager/Feedback/ProvideController.php @@ -0,0 +1,71 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/src/Controller/HouseManager/IndexController.php b/src/Controller/HouseManager/IndexController.php index 0a28f5d..5942008 100644 --- a/src/Controller/HouseManager/IndexController.php +++ b/src/Controller/HouseManager/IndexController.php @@ -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'); } } \ No newline at end of file diff --git a/src/Entity/Disposition.php b/src/Entity/Disposition.php index b74cdd2..17ab624 100644 --- a/src/Entity/Disposition.php +++ b/src/Entity/Disposition.php @@ -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) diff --git a/src/Entity/Feedback.php b/src/Entity/Feedback.php index 3ccbae8..335c7a4 100644 --- a/src/Entity/Feedback.php +++ b/src/Entity/Feedback.php @@ -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; + } } diff --git a/src/Event/FeedbackProvidedEvent.php b/src/Event/FeedbackProvidedEvent.php new file mode 100644 index 0000000..b5cc91f --- /dev/null +++ b/src/Event/FeedbackProvidedEvent.php @@ -0,0 +1,20 @@ +feedback; + } +} \ No newline at end of file diff --git a/src/Form/FeedbackRatingsType.php b/src/Form/FeedbackRatingsType.php new file mode 100644 index 0000000..3a1f3b4 --- /dev/null +++ b/src/Form/FeedbackRatingsType.php @@ -0,0 +1,32 @@ +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) + ; + } +} \ No newline at end of file diff --git a/src/Form/FeedbackType.php b/src/Form/FeedbackType.php new file mode 100644 index 0000000..b56fd13 --- /dev/null +++ b/src/Form/FeedbackType.php @@ -0,0 +1,41 @@ +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) + ; + } +} \ No newline at end of file diff --git a/src/Menu/AbstractMenuBuilder.php b/src/Menu/AbstractMenuBuilder.php index 40652ee..5de15e5 100644 --- a/src/Menu/AbstractMenuBuilder.php +++ b/src/Menu/AbstractMenuBuilder.php @@ -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(); diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php index ca3ec3c..82bd5c0 100644 --- a/src/Menu/AdminMenuBuilder.php +++ b/src/Menu/AdminMenuBuilder.php @@ -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; diff --git a/src/Menu/HouseManagerMenuBuilder.php b/src/Menu/HouseManagerMenuBuilder.php new file mode 100644 index 0000000..3943da1 --- /dev/null +++ b/src/Menu/HouseManagerMenuBuilder.php @@ -0,0 +1,44 @@ + '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; + } +} \ No newline at end of file diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index b25a46b..d0eb34c 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -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(); + } } diff --git a/src/Repository/FeedbackRepository.php b/src/Repository/FeedbackRepository.php index 9bc8c22..47ab496 100644 --- a/src/Repository/FeedbackRepository.php +++ b/src/Repository/FeedbackRepository.php @@ -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() -// ; -// } } diff --git a/src/Security/Voter/DispositionVoter.php b/src/Security/Voter/DispositionVoter.php index 21cd1e9..05dad8a 100644 --- a/src/Security/Voter/DispositionVoter.php +++ b/src/Security/Voter/DispositionVoter.php @@ -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: diff --git a/templates/admin/teamer/index.html.twig b/templates/admin/teamer/index.html.twig index 7aa2a30..f7e7eb5 100644 --- a/templates/admin/teamer/index.html.twig +++ b/templates/admin/teamer/index.html.twig @@ -1,11 +1,11 @@ {% extends 'admin/layout.html.twig' %} -{% block title %}Teamer:innenübersicht{% endblock %} +{% block title %}Teamerübersicht{% endblock %} {% block content %}

- Teamer:innenübersicht + Teamerübersicht

+
+
+ + + + + + + + + + + + + + {% for disposition in pagination %} + {% set teamer = disposition.teamer %} + {% set assignment = disposition.assignment %} + + + + + + + + + + {% else %} + + {% endfor %} + +
+ {{ knp_pagination_sortable(pagination, 'Name', 'teamer.lastName') }} + + Vorname + + {{ knp_pagination_sortable(pagination, 'Einsatz­zeitraum', 'destination.dateFrom') }} + + {{ knp_pagination_sortable(pagination, 'Destination', 'destination.product') }} + + {{ knp_pagination_sortable(pagination, 'Jobprofil', 'job_profile.name') }} +
+ {% if teamer.photo %} + {{ teamer.firstName }} + {% else %} +
+ {{ icon('user', 'w-10 h-10 text-gray-200') }} +
+ {% endif %} +
+ + {{ teamer.lastName }} + + + + {{ teamer.firstName }} + + + + {{ assignment.effectivePeriod.start|date('d.m.Y') }} - +
+ {{ assignment.effectivePeriod.end|date('d.m.Y') }} +
+
+ + {{ icon('flag-' ~ assignment.destination.country, 'w-5 h-5 shrink-0') }} +
+ {{ assignment.destination.product }} +
+ {{ assignment.destination.hotel }} +
+
+
+ + {{ assignment.jobProfile.name }} + + + +
+ Keine Daten... +
+ {{ knp_pagination_render(pagination) }} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/house_manager/feedback/provide.html.twig b/templates/house_manager/feedback/provide.html.twig new file mode 100644 index 0000000..ede6cdf --- /dev/null +++ b/templates/house_manager/feedback/provide.html.twig @@ -0,0 +1,31 @@ +{% extends 'house_manager/layout.html.twig' %} + +{% block title %}Feedback abgeben{% endblock %} + +{% block content %} +

+ Dein Feedback zu {{ disposition.teamer }} +

+
+ {{ disposition.assignment.jobProfile.name }}, {{ disposition.assignment.destination }} +
+ {{ form_start(form) }} +
+ {% for child in form.ratings.children %} +
+

+ {{ child.vars.label }} +

+ {{ form_widget(child) }} +
+ {% endfor %} +
+ {{ form_row(form.comment) }} +
+
+ + {{ form_rest(form) }} + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/templates/house_manager/index.html.twig b/templates/house_manager/index.html.twig new file mode 100644 index 0000000..3bec960 --- /dev/null +++ b/templates/house_manager/index.html.twig @@ -0,0 +1,4 @@ +{% extends 'house_manager/layout.html.twig' %} + +{% block content %} +{% endblock %} \ No newline at end of file diff --git a/templates/house_manager/layout.html.twig b/templates/house_manager/layout.html.twig new file mode 100644 index 0000000..23b1533 --- /dev/null +++ b/templates/house_manager/layout.html.twig @@ -0,0 +1,9 @@ +{% 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 %}