From 908b904b67f41a1be705efb09eb3207f2ad9275d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Sun, 5 Nov 2023 18:19:58 +0100 Subject: [PATCH] WIP: Implement feedback functionality --- migrations/Version20231105160706.php | 37 ++++++++++++ .../Teamer/RecentAssignmentsController.php | 28 ++++++++- .../Feedback/ProvideController.php | 4 +- src/Entity/Feedback.php | 33 ++++++++++ src/Form/AutocompleteEntityType.php | 60 ++++++++++--------- .../DataTransformer/EntityToIdTransformer.php | 38 ------------ .../EntityToIdentifierTransformer.php | 54 +++++++++++++++++ .../FeedbackToRatingsTransformer.php | 33 ++++++++++ src/Form/FeedbackType.php | 8 +++ src/Form/HiddenEntityType.php | 10 +++- src/Repository/DispositionRepository.php | 7 ++- src/Security/Voter/DispositionVoter.php | 2 +- src/Security/Voter/FeedbackVoter.php | 44 ++++++++++++++ templates/admin/teamer/index.html.twig | 4 +- .../admin/teamer/recent_assignments.html.twig | 59 ++++++++++++++++++ 15 files changed, 346 insertions(+), 75 deletions(-) create mode 100644 migrations/Version20231105160706.php delete mode 100644 src/Form/DataTransformer/EntityToIdTransformer.php create mode 100644 src/Form/DataTransformer/EntityToIdentifierTransformer.php create mode 100644 src/Form/DataTransformer/FeedbackToRatingsTransformer.php create mode 100644 src/Security/Voter/FeedbackVoter.php diff --git a/migrations/Version20231105160706.php b/migrations/Version20231105160706.php new file mode 100644 index 0000000..47f4085 --- /dev/null +++ b/migrations/Version20231105160706.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE feedback ADD status VARCHAR(32) NOT NULL'); + } + + public function postUp(Schema $schema): void + { + $this->connection->executeQuery('UPDATE feedback SET status=?', [Feedback::STATUS_NEW]); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE feedback DROP status'); + } +} diff --git a/src/Controller/Admin/Teamer/RecentAssignmentsController.php b/src/Controller/Admin/Teamer/RecentAssignmentsController.php index 9bce49d..e1986a5 100644 --- a/src/Controller/Admin/Teamer/RecentAssignmentsController.php +++ b/src/Controller/Admin/Teamer/RecentAssignmentsController.php @@ -3,19 +3,45 @@ namespace App\Controller\Admin\Teamer; use App\Entity\Teamer; +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 RecentAssignmentsController extends AbstractController { + public function __construct( + private readonly DispositionRepository $dispositionRepository, + private readonly PaginatorInterface $paginator + ) { + } + #[Route('/admin/teamer/recent-assignments/{uuid}', name: 'app_admin_teamer_recent_assignments')] #[IsGranted('ROLE_ADMINISTRATIVE')] - public function index(Teamer $teamer): Response + public function index(Teamer $teamer, Request $request): Response { + $query = $this + ->dispositionRepository + ->getRecentQuery($teamer) + ; + + $pagination = $this->paginator->paginate( + $query, + $request->query->getInt('page', 1), + 10, + [ + 'defaultSortFieldName' => 'destination.dateFrom', + 'defaultSortDirection' => 'asc', + ] + ); + + return $this->render('admin/teamer/recent_assignments.html.twig', [ 'teamer' => $teamer, + 'pagination' => $pagination, ]); } } \ No newline at end of file diff --git a/src/Controller/HouseManager/Feedback/ProvideController.php b/src/Controller/HouseManager/Feedback/ProvideController.php index f5e1697..c7d3390 100644 --- a/src/Controller/HouseManager/Feedback/ProvideController.php +++ b/src/Controller/HouseManager/Feedback/ProvideController.php @@ -30,13 +30,13 @@ class ProvideController extends AbstractController { $assignment = $disposition->getAssignment(); $destination = $assignment->getDestination(); - $feebackSet = $assignment + $feedbackSet = $assignment ->getJobProfile() ->getFeedbackSet() ; $feedback = new Feedback(); - $form = $this->createForm(FeedbackType::class, $feedback, ['feedback_set' => $feebackSet]); + $form = $this->createForm(FeedbackType::class, $feedback, ['feedback_set' => $feedbackSet]); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { diff --git a/src/Entity/Feedback.php b/src/Entity/Feedback.php index 335c7a4..6a1ff94 100644 --- a/src/Entity/Feedback.php +++ b/src/Entity/Feedback.php @@ -15,6 +15,9 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface use BlameableEntity; use TimestampableEntity; + public const STATUS_NEW = 'new'; + public const STATUS_PUBLISHED = 'published'; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -23,6 +26,9 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface #[ORM\Column(type: 'string', length: 36, unique: true)] private string $uuid; + #[ORM\Column(length: 32)] + private ?string $status = self::STATUS_NEW; + #[ORM\ManyToOne(inversedBy: 'feedback')] private ?Teamer $teamer = null; @@ -56,6 +62,18 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface return $this->uuid; } + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(string $status): static + { + $this->status = $status; + + return $this; + } + public function getTeamer(): ?Teamer { return $this->teamer; @@ -104,6 +122,21 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface return $this; } + public function getAverageRating(): ?int + { + if (0 === count($this->ratings)) { + return null; + } + + $sum = 0; + + foreach ($this->ratings as $rating) { + $sum += $rating['mark']; + } + + return round($sum/count($this->ratings)); + } + public function getComment(): ?string { return $this->comment; diff --git a/src/Form/AutocompleteEntityType.php b/src/Form/AutocompleteEntityType.php index 0d2302e..e1e789b 100644 --- a/src/Form/AutocompleteEntityType.php +++ b/src/Form/AutocompleteEntityType.php @@ -2,7 +2,7 @@ namespace App\Form; -use App\Form\DataTransformer\EntityToIdTransformer; +use App\Form\DataTransformer\EntityToIdentifierTransformer; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -21,27 +21,33 @@ class AutocompleteEntityType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - $dataTransformer = new EntityToIdTransformer($this->entityManager, $options['class']); + $dataTransformer = new EntityToIdentifierTransformer( + $this->entityManager, + $options['class'], + $options['identifier'] + ); $builder->addModelTransformer($dataTransformer); } public function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired(['class']); - - $resolver->setDefaults([ - 'endpoint_route' => null, - 'choices' => [], - 'compound' => false, - 'label_property' => 'label', - 'label_function' => null, - 'placeholder' => null, - 'endpoint_parameters' => [], - 'controller_action' => null, - ]); - - $resolver->setAllowedTypes('choices', 'array'); - $resolver->setAllowedTypes('label_function', ['null', 'callable']); + $resolver + ->setRequired(['class']) + ->setDefaults([ + 'endpoint_route' => null, + 'choices' => [], + 'compound' => false, + 'label_property' => 'label', + 'label_function' => null, + 'placeholder' => null, + 'endpoint_parameters' => [], + 'controller_action' => null, + 'identifier' => 'id', + ]) + ->setAllowedTypes('choices', 'array') + ->setAllowedTypes('label_function', ['null', 'callable']) + ->setAllowedValues('identifier', ['id', 'uuid']) + ; } public function buildView(FormView $view, FormInterface $form, array $options): void @@ -57,17 +63,17 @@ class AutocompleteEntityType extends AbstractType $view->vars['initial_label'] = ''; $view->vars['action'] = $options['controller_action']; - $entity = $form->getData(); + if (null === $entity = $form->getData()) { + return; + } - if (null !== $entity) { - if (null !== $options['label_function']) { - $labelFunction = $options['label_function']; - $view->vars['initial_label'] = $labelFunction($entity); - } else { - $getter = 'get'.ucfirst($options['label_property']); - if (method_exists($entity, $getter)) { - $view->vars['initial_label'] = $entity->$getter(); - } + if (null !== $options['label_function']) { + $labelFunction = $options['label_function']; + $view->vars['initial_label'] = $labelFunction($entity); + } else { + $getter = 'get'.ucfirst($options['label_property']); + if (method_exists($entity, $getter)) { + $view->vars['initial_label'] = call_user_func([$entity, $getter]); } } } diff --git a/src/Form/DataTransformer/EntityToIdTransformer.php b/src/Form/DataTransformer/EntityToIdTransformer.php deleted file mode 100644 index 9760995..0000000 --- a/src/Form/DataTransformer/EntityToIdTransformer.php +++ /dev/null @@ -1,38 +0,0 @@ -getId(); - } - - public function reverseTransform($value) - { - if (empty($value)) { - return null; - } - - $entity = $this->entityManager->getRepository($this->class)->find($value); - - if (null === $entity) { - throw new TransformationFailedException(sprintf('No %s with id %d found', $this->class, $value)); - } - - return $entity; - } -} diff --git a/src/Form/DataTransformer/EntityToIdentifierTransformer.php b/src/Form/DataTransformer/EntityToIdentifierTransformer.php new file mode 100644 index 0000000..d7e6883 --- /dev/null +++ b/src/Form/DataTransformer/EntityToIdentifierTransformer.php @@ -0,0 +1,54 @@ +identifier = $identifier; + } + + public function transform($value) + { + if (null === $value) { + return null; + } + + $getter = 'get'.ucfirst($this->identifier); + + return call_user_func([$value, $getter]); + } + + public function reverseTransform($value) + { + if (empty($value)) { + return null; + } + + $entity = $this + ->entityManager + ->getRepository($this->class) + ->findOneBy([$this->identifier => $value]) + ; + + if (null === $entity) { + throw new TransformationFailedException(sprintf('No %s with %s %s found', $this->class, $this->identifier, $value)); + } + + return $entity; + } +} diff --git a/src/Form/DataTransformer/FeedbackToRatingsTransformer.php b/src/Form/DataTransformer/FeedbackToRatingsTransformer.php new file mode 100644 index 0000000..f76201f --- /dev/null +++ b/src/Form/DataTransformer/FeedbackToRatingsTransformer.php @@ -0,0 +1,33 @@ +feedbackSet->getRatings() as $index => $rating) { + $feedbackRatings[] = [ + 'rating' => $rating, + 'mark' => $value['rating_'.$index], + ]; + } + + return $feedbackRatings; + } +} \ No newline at end of file diff --git a/src/Form/FeedbackType.php b/src/Form/FeedbackType.php index b56fd13..829399d 100644 --- a/src/Form/FeedbackType.php +++ b/src/Form/FeedbackType.php @@ -4,6 +4,7 @@ namespace App\Form; use App\Entity\Feedback; use App\Entity\FeedbackSet; +use App\Form\DataTransformer\FeedbackToRatingsTransformer; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; @@ -26,6 +27,13 @@ class FeedbackType extends AbstractType ], ]) ; + + // This is most probably more suitable for DatamapperInterface + // but it works very well for the time being + $builder + ->get('ratings') + ->addModelTransformer(new FeedbackToRatingsTransformer($options['feedback_set'])) + ; } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/HiddenEntityType.php b/src/Form/HiddenEntityType.php index d55006c..2a8705f 100644 --- a/src/Form/HiddenEntityType.php +++ b/src/Form/HiddenEntityType.php @@ -2,7 +2,7 @@ namespace App\Form; -use App\Form\DataTransformer\EntityToIdTransformer; +use App\Form\DataTransformer\EntityToIdentifierTransformer; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -19,7 +19,11 @@ class HiddenEntityType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { - $transformer = new EntityToIdTransformer($this->entityManager, $options['class']); + $transformer = new EntityToIdentifierTransformer( + $this->entityManager, + $options['class'], + $options['identifier'] + ); $builder->addModelTransformer($transformer); } @@ -34,7 +38,9 @@ class HiddenEntityType extends AbstractType ->setRequired(['class']) ->setDefaults([ 'invalid_message' => 'The entity does not exist.', + 'identifier' => 'id', ]) + ->setAllowedValues('identifier', ['id', 'uuid']) ; } diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index d0eb34c..661a4e0 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -57,10 +57,11 @@ class DispositionRepository extends ServiceEntityRepository $qb = $this->createQueryBuilder('disposition'); return $qb - ->select('disposition', 'assignment', 'job_profile', 'destination') + ->select('disposition', 'assignment', 'job_profile', 'destination', 'feedback') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.jobProfile', 'job_profile') ->innerJoin('assignment.destination', 'destination') + ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->andX( $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->orX( @@ -99,12 +100,14 @@ class DispositionRepository extends ServiceEntityRepository $qb = $this->createQueryBuilder('disposition'); return $qb - ->select('disposition', 'assignment', 'destination') + ->select('disposition', 'assignment', 'destination', 'feedback') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') + ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->eq('disposition.teamer', ':teamer')) ->setParameter('teamer', $teamer) ->orderBy('destination.dateFrom', 'DESC') + ->setMaxResults($limit) ->getQuery() ->getResult() ; diff --git a/src/Security/Voter/DispositionVoter.php b/src/Security/Voter/DispositionVoter.php index 05dad8a..4923838 100644 --- a/src/Security/Voter/DispositionVoter.php +++ b/src/Security/Voter/DispositionVoter.php @@ -28,7 +28,7 @@ class DispositionVoter extends Voter return false; } - return in_array($attribute, [static::VIEW, static::EDIT, static::DELETE, static::CONTRACT, static::INVOICE]); + return in_array($attribute, [static::VIEW, static::EDIT, static::DELETE, static::CONTRACT, static::INVOICE, static::FEEDBACK]); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool diff --git a/src/Security/Voter/FeedbackVoter.php b/src/Security/Voter/FeedbackVoter.php new file mode 100644 index 0000000..1452196 --- /dev/null +++ b/src/Security/Voter/FeedbackVoter.php @@ -0,0 +1,44 @@ +getRoleNames())) { + return true; + } + + if (in_array('ROLE_HOUSE_MANAGER', $token->getRoleNames())) { + return static::VIEW === $attribute; + } + + if (in_array('ROLE_TEAMER', $token->getRoleNames())) { + /** @var User $user */ + $user = $token->getUser(); + $teamer = $user->getTeamer(); + + return $feedback->getTeamer() === $teamer; + } + + return false; + } +} \ No newline at end of file diff --git a/templates/admin/teamer/index.html.twig b/templates/admin/teamer/index.html.twig index f7e7eb5..7fe4923 100644 --- a/templates/admin/teamer/index.html.twig +++ b/templates/admin/teamer/index.html.twig @@ -70,12 +70,12 @@ {% endif %} - + {{ teamer.lastName }} - + {{ teamer.firstName }} diff --git a/templates/admin/teamer/recent_assignments.html.twig b/templates/admin/teamer/recent_assignments.html.twig index f2df8a5..3620f08 100644 --- a/templates/admin/teamer/recent_assignments.html.twig +++ b/templates/admin/teamer/recent_assignments.html.twig @@ -11,6 +11,65 @@

Vergangene Einsätze & Feeback {{ teamer }}

+
+
+ + + + + + + + + + + {% for disposition in pagination %} + + + + + + + {% else %} + + {% endfor %} + +
+ Einsatz + + Gesamtnote + + Kommentar +
+ {{ disposition.assignment.destination.dateFrom|date('d.m.Y') }} +
+ {{ disposition.assignment.destination.product }} +
+ {{ disposition.assignment.destination.hotel }} +
+ {% if disposition.feedback %} + {{ disposition.feedback.averageRating }} + {% else %} + - + {% endif %} + + {% if disposition.feedback %} +
+ {{ disposition.feedback.comment|default('-') }} +
+ {% else %} + - + {% endif %} +
+ +
+ Keine Daten... +
+ {{ knp_pagination_render(pagination) }} +
+
{% endblock %} \ No newline at end of file