Compare commits
3
Commits
6795a668da
...
dfa3e63926
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfa3e63926 | ||
|
|
43968e4e7b | ||
|
|
4ae23bf8a1 |
No files matched your search
@@ -37,8 +37,8 @@ class ProvideController extends AbstractController
|
||||
// Feedback provided by admins is published instantly
|
||||
$feedback->setStatus(Feedback::STATUS_PUBLISHED);
|
||||
|
||||
if (null !== $form->get('authorOverride')->getData()) {
|
||||
$feedback->setAuthor($form->get('authorOverride')->getData());
|
||||
if (null !== $author = $form->get('authorOverride')->getData()) {
|
||||
$feedback->setAuthor($author->getFullName());
|
||||
}
|
||||
|
||||
$this->entityManager->persist($feedback);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller\Administrative\Availability;
|
||||
|
||||
use App\Repository\AvailabilityRepository;
|
||||
use App\Service\Common\AvailabilityFilterHandler;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -15,6 +16,7 @@ class IndexController extends AbstractController
|
||||
public function __construct(
|
||||
private readonly AvailabilityRepository $availabilityRepository,
|
||||
private readonly PaginatorInterface $paginator,
|
||||
private readonly AvailabilityFilterHandler $filterHandler,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -22,9 +24,11 @@ class IndexController extends AbstractController
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$filterDto = $this->filterHandler->getFilterSettings();
|
||||
|
||||
$query = $this
|
||||
->availabilityRepository
|
||||
->getListQuery(true)
|
||||
->getListQuery(true, $filterDto)
|
||||
;
|
||||
|
||||
$pagination = $this->paginator->paginate(
|
||||
@@ -39,6 +43,7 @@ class IndexController extends AbstractController
|
||||
|
||||
return $this->render('administrative/availability/index.html.twig', [
|
||||
'pagination' => $pagination,
|
||||
'filterDto' => $filterDto,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controller\Administrative\Feedback;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use App\Repository\FeedbackRepository;
|
||||
use App\Service\Common\FeedbackFilterHandler;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
@@ -46,4 +47,15 @@ class IndexController extends AbstractController
|
||||
'filterDto' => $filterDto,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/administrative/feedback/teamer/{uuid}', name: 'app_administrative_feedback_index_teamer')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function byTeamer(Teamer $teamer): Response
|
||||
{
|
||||
// The link means "show me this teamer's feedback", so previously stored
|
||||
// criteria must not silently hide any of it.
|
||||
$this->filterHandler->applyTeamerFilter($teamer);
|
||||
|
||||
return $this->redirectToRoute('app_administrative_feedback_index');
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,12 @@ class ProvideController extends AbstractController
|
||||
$feedback->setStatus(Feedback::STATUS_PUBLISHED);
|
||||
}
|
||||
|
||||
if (true === $form->has('author')) {
|
||||
/** @var User $author */
|
||||
$author = $form->get('author')->getData();
|
||||
$feedback->setAuthor($author->getFullName());
|
||||
}
|
||||
|
||||
$teamer = $disposition->getTeamer();
|
||||
$teamer->addFeedback($feedback);
|
||||
$disposition->setFeedback($feedback);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Common;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Form\AvailabilityFilterType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Repository\JobProfileRepository;
|
||||
use App\Service\Common\AvailabilityFilterHandler;
|
||||
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;
|
||||
|
||||
class AvailabilityFilterController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly AvailabilityFilterHandler $filterHandler,
|
||||
private readonly JobProfileRepository $jobProfileRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/common/availability/filter', name: 'app_common_availability_filter')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$formData = $this->filterHandler->getFilterSettings();
|
||||
$formOptions = [
|
||||
'job_profiles' => $this->jobProfileRepository->getList(),
|
||||
];
|
||||
|
||||
$form = $this->createForm(AvailabilityFilterType::class, $formData, $formOptions);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->filterHandler->handleRequest($form);
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_availability_index');
|
||||
$returnUrl = $this->removeQueryParameters($returnUrl, ['page']);
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('common/modal_availability_filter.html.twig', [
|
||||
'filterForm' => $form->createView(),
|
||||
'filterDto' => $form->getData(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/common/availability/filter/reset', name: 'app_common_availability_filter_reset')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function reset(Request $request): Response
|
||||
{
|
||||
$this->filterHandler->resetFilterSettings();
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_availability_index');
|
||||
|
||||
return $this->redirect($returnUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
abstract class AbstractFeedbackType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* The ratings depend on the feedback set, which one flow knows upfront and the other only once
|
||||
* it has been picked - hence the union type, as the fields are added to a builder in the first
|
||||
* case and to an already built form in the second.
|
||||
*/
|
||||
protected function addFeedbackFields(FormBuilderInterface|FormInterface $form, FeedbackSet $feedbackSet): void
|
||||
{
|
||||
$form
|
||||
->add('ratings', FeedbackRatingsType::class, [
|
||||
'label' => false,
|
||||
'feedback_set' => $feedbackSet,
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
'label' => 'Kommentar (öffentlich)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
'help' => 'Bitte formuliere deinen Kommentar so, dass er an das Team weitergegeben werden kann',
|
||||
])
|
||||
->add('commentInternal', TextareaType::class, [
|
||||
'label' => 'Kommentar (intern)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Feedback::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,17 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Feedback;
|
||||
use App\Entity\FeedbackSet;
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\User;
|
||||
use App\Repository\FeedbackSetRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AdminFeedbackType extends AbstractType
|
||||
class AdminFeedbackType extends AbstractFeedbackType
|
||||
{
|
||||
public function __construct(private readonly FeedbackSetRepository $feedbackSetRepository)
|
||||
{
|
||||
@@ -34,7 +30,7 @@ class AdminFeedbackType extends AbstractType
|
||||
])
|
||||
->add('authorOverride', AutocompleteEntityType::class, [
|
||||
'mapped' => false,
|
||||
'label' => 'abweichender Absender',
|
||||
'label' => 'Feedbackgeber:in',
|
||||
'class' => User::class,
|
||||
'label_property' => 'lastName',
|
||||
'label_function' => fn (User $user) => (string) $user,
|
||||
@@ -57,7 +53,7 @@ class AdminFeedbackType extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addDynamicForm($feedbackSet, $form);
|
||||
$this->addFeedbackFields($form, $feedbackSet);
|
||||
})
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
|
||||
$data = $event->getData();
|
||||
@@ -67,47 +63,26 @@ class AdminFeedbackType extends AbstractType
|
||||
return;
|
||||
}
|
||||
|
||||
$feedbackSet = $this->feedbackSetRepository->find($data['feedbackSet']);
|
||||
$this->addDynamicForm($feedbackSet, $form);
|
||||
})
|
||||
;
|
||||
}
|
||||
if (null === $feedbackSet = $this->feedbackSetRepository->find($data['feedbackSet'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
private function addDynamicForm(FeedbackSet $feedbackSet, FormInterface $form): void
|
||||
{
|
||||
$form
|
||||
->add('ratings', FeedbackRatingsType::class, [
|
||||
'label' => false,
|
||||
'feedback_set' => $feedbackSet,
|
||||
])
|
||||
->add('comment', TextareaType::class, [
|
||||
'label' => 'Kommentar (öffentlich)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
'help' => 'Bitte formuliere deinen Kommentar so, dass er an das Team weitergegeben werden kann',
|
||||
])
|
||||
->add('commentInternal', TextareaType::class, [
|
||||
'label' => 'Kommentar (intern)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
])
|
||||
$this->addFeedbackFields($form, $feedbackSet);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => Feedback::class,
|
||||
'validation_groups' => [
|
||||
'Default',
|
||||
'admin',
|
||||
],
|
||||
])
|
||||
;
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver->setDefaults([
|
||||
// the disposition-driven flow guarantees teamer and feedback set structurally, here
|
||||
// they are picked by hand and must be validated
|
||||
'validation_groups' => [
|
||||
'Default',
|
||||
'admin',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\JobProfile;
|
||||
use App\Entity\Teamer;
|
||||
use App\Model\AvailabilityFilterDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AvailabilityFilterType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('teamer', AutocompleteEntityType::class, [
|
||||
'label' => 'Teamer',
|
||||
'class' => Teamer::class,
|
||||
'label_property' => 'name',
|
||||
'label_function' => fn (Teamer $teamer) => (string) $teamer,
|
||||
'endpoint_route' => 'app_admin_autocomplete_teamer',
|
||||
])
|
||||
->add('jobProfiles', MultiselectEntityType::class, [
|
||||
'label' => 'Job-Profil',
|
||||
'class' => JobProfile::class,
|
||||
'required' => false,
|
||||
'empty_label' => 'nicht filtern',
|
||||
'choice_label' => 'name',
|
||||
'choices' => $options['job_profiles'],
|
||||
])
|
||||
->add('dateFrom', DatepickerType::class, [
|
||||
'label' => 'Zeitraum von',
|
||||
'attr' => [
|
||||
'placeholder' => 'nicht filtern',
|
||||
],
|
||||
])
|
||||
->add('dateTo', DatepickerType::class, [
|
||||
'label' => 'Zeitraum bis',
|
||||
'attr' => [
|
||||
'placeholder' => 'nicht filtern',
|
||||
],
|
||||
])
|
||||
->add('apply', SubmitType::class, [
|
||||
'label' => 'filtern',
|
||||
])
|
||||
->add('reset', SubmitType::class, [
|
||||
'label' => 'reset',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => AvailabilityFilterDto::class,
|
||||
'job_profiles' => [],
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use App\Model\FeedbackFilterDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
@@ -15,6 +16,14 @@ class FeedbackFilterType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('teamer', AutocompleteEntityType::class, [
|
||||
'label' => 'Teamer',
|
||||
'required' => false,
|
||||
'class' => Teamer::class,
|
||||
'label_property' => 'name',
|
||||
'label_function' => fn (Teamer $teamer) => (string) $teamer,
|
||||
'endpoint_route' => 'app_admin_autocomplete_teamer',
|
||||
])
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Name',
|
||||
'required' => false,
|
||||
|
||||
+52
-28
@@ -2,46 +2,70 @@
|
||||
|
||||
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 App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class FeedbackType extends AbstractType
|
||||
class FeedbackType extends AbstractFeedbackType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly UserRepository $userRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
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 (öffentlich)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
'help' => 'Bitte formuliere deinen Kommentar so, dass er an das Team weitergegeben werden kann',
|
||||
])
|
||||
->add('commentInternal', TextareaType::class, [
|
||||
'label' => 'Kommentar (intern)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'rows' => 5,
|
||||
],
|
||||
])
|
||||
;
|
||||
if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
|
||||
$this->addAuthorField($builder);
|
||||
}
|
||||
|
||||
$this->addFeedbackFields($builder, $options['feedback_set']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Feedback::$author is a name snapshot rather than a relation, so the field stays unmapped and
|
||||
* the controller writes the chosen user's full name.
|
||||
*/
|
||||
private function addAuthorField(FormBuilderInterface $builder): void
|
||||
{
|
||||
$roles = array_diff(array_keys(User::ROLES), ['ROLE_TEAMER']);
|
||||
|
||||
$choices = $this->userRepository->getUsersByRoles($roles);
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
// a user granted the role by the hierarchy alone would not be among the choices
|
||||
$choiceIds = array_map(fn (User $choice) => $choice->getId(), $choices);
|
||||
|
||||
if (false === in_array($user->getId(), $choiceIds, true)) {
|
||||
array_unshift($choices, $user);
|
||||
}
|
||||
|
||||
$builder->add('author', EntityType::class, [
|
||||
'label' => 'Feedbackgeber:in',
|
||||
'mapped' => false,
|
||||
'class' => User::class,
|
||||
'choices' => $choices,
|
||||
'choice_label' => 'fullName',
|
||||
'data' => $user,
|
||||
'constraints' => [
|
||||
new Assert\NotNull(message: 'Bitte wähle eine:n Feedbackgeber:in'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
parent::configureOptions($resolver);
|
||||
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => Feedback::class,
|
||||
])
|
||||
->setRequired(['feedback_set'])
|
||||
->setAllowedTypes('feedback_set', FeedbackSet::class)
|
||||
;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
|
||||
class AvailabilityFilterDto extends AbstractFilterDto
|
||||
{
|
||||
protected ?\DateTimeImmutable $dateFrom = null;
|
||||
protected ?\DateTimeImmutable $dateTo = null;
|
||||
protected ?Teamer $teamer = null;
|
||||
protected array $jobProfiles = [];
|
||||
|
||||
public function getDateFrom(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->dateFrom;
|
||||
}
|
||||
|
||||
public function setDateFrom(?\DateTimeImmutable $dateFrom): static
|
||||
{
|
||||
$this->dateFrom = $dateFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateTo(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->dateTo;
|
||||
}
|
||||
|
||||
public function setDateTo(?\DateTimeImmutable $dateTo): static
|
||||
{
|
||||
$this->dateTo = $dateTo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTeamer(): ?Teamer
|
||||
{
|
||||
return $this->teamer;
|
||||
}
|
||||
|
||||
public function setTeamer(?Teamer $teamer): static
|
||||
{
|
||||
$this->teamer = $teamer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJobProfiles(): array
|
||||
{
|
||||
return $this->jobProfiles;
|
||||
}
|
||||
|
||||
public function setJobProfiles(array $jobProfiles): static
|
||||
{
|
||||
$this->jobProfiles = $jobProfiles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,28 @@
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
|
||||
class FeedbackFilterDto extends AbstractFilterDto
|
||||
{
|
||||
protected ?Teamer $teamer = null;
|
||||
protected ?string $name = null;
|
||||
protected ?\DateTimeImmutable $dateFrom = null;
|
||||
protected ?\DateTimeImmutable $dateTo = null;
|
||||
protected ?int $averageRating = null;
|
||||
|
||||
public function getTeamer(): ?Teamer
|
||||
{
|
||||
return $this->teamer;
|
||||
}
|
||||
|
||||
public function setTeamer(?Teamer $teamer): static
|
||||
{
|
||||
$this->teamer = $teamer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Repository;
|
||||
|
||||
use App\Entity\Availability;
|
||||
use App\Entity\Teamer;
|
||||
use App\Model\AvailabilityFilterDto;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
@@ -23,7 +24,7 @@ class AvailabilityRepository extends ServiceEntityRepository
|
||||
parent::__construct($registry, Availability::class);
|
||||
}
|
||||
|
||||
public function getListQuery(bool $custom = false): Query
|
||||
public function getListQuery(bool $custom = false, ?AvailabilityFilterDto $filterDto = null): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('availability');
|
||||
|
||||
@@ -49,6 +50,50 @@ class AvailabilityRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $filterDto) {
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
if (null !== $dateFrom = $filterDto->getDateFrom()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->gte('availability.dateFrom', ':dateFrom'))
|
||||
->setParameter('dateFrom', $dateFrom)
|
||||
;
|
||||
}
|
||||
|
||||
if (null !== $dateTo = $filterDto->getDateTo()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->lte('availability.dateTo', ':dateTo'))
|
||||
->setParameter('dateTo', $dateTo)
|
||||
;
|
||||
}
|
||||
|
||||
if (null !== $teamer = $filterDto->getTeamer()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('availability.owner', ':teamer'))
|
||||
->setParameter('teamer', $teamer)
|
||||
;
|
||||
}
|
||||
|
||||
if (0 < count($jobProfiles = $filterDto->getJobProfiles())) {
|
||||
// a join on the job-profiles would multiply the availability whenever its
|
||||
// owner holds more than one of the selected profiles, which would break
|
||||
// the pagination count, so the owner is matched against a subquery instead
|
||||
$subQuery = $this
|
||||
->getEntityManager()
|
||||
->createQueryBuilder()
|
||||
->select('profileTeamer.id')
|
||||
->from(Teamer::class, 'profileTeamer')
|
||||
->innerJoin('profileTeamer.jobProfiles', 'filterJobProfile')
|
||||
->where('filterJobProfile IN (:jobProfiles)')
|
||||
;
|
||||
|
||||
$qb
|
||||
->andWhere($qb->expr()->in('availability.owner', $subQuery->getDQL()))
|
||||
->setParameter('jobProfiles', $jobProfiles)
|
||||
;
|
||||
}
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ class FeedbackRepository extends ServiceEntityRepository
|
||||
->innerJoin('teamer.user', 'user')
|
||||
;
|
||||
|
||||
if (null !== $teamer = $filterDto->getTeamer()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('feedback.teamer', ':teamer'))
|
||||
->setParameter('teamer', $teamer)
|
||||
;
|
||||
}
|
||||
|
||||
if (null !== $name = $filterDto->getName()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->orX(
|
||||
|
||||
@@ -61,6 +61,35 @@ class UserRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active users holding at least one of the given roles.
|
||||
*
|
||||
* @param string[] $roles
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
public function getUsersByRoles(array $roles): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('user');
|
||||
|
||||
$roleMatches = $qb->expr()->orX();
|
||||
|
||||
foreach ($roles as $index => $role) {
|
||||
$roleMatches->add('JSON_CONTAINS(user.roles, :role_'.$index.') = 1');
|
||||
$qb->setParameter('role_'.$index, json_encode($role));
|
||||
}
|
||||
|
||||
return $qb
|
||||
->andWhere($roleMatches)
|
||||
->andWhere($qb->expr()->isNull('user.deletedAt'))
|
||||
->andWhere($qb->expr()->isNull('user.disabledAt'))
|
||||
->orderBy('user.lastName', 'ASC')
|
||||
->addOrderBy('user.firstName', 'ASC')
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|string $hotelCode
|
||||
*
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Common;
|
||||
|
||||
use App\Entity\JobProfile;
|
||||
use App\Entity\Teamer;
|
||||
use App\Model\AbstractFilterDto;
|
||||
use App\Model\AvailabilityFilterDto;
|
||||
|
||||
class AvailabilityFilterHandler extends AbstractFilterHandler
|
||||
{
|
||||
protected string $namespace = 'filter:availability';
|
||||
protected string $modelClass = AvailabilityFilterDto::class;
|
||||
|
||||
public function getFilterSettings(): AvailabilityFilterDto
|
||||
{
|
||||
$filterDto = new $this->modelClass();
|
||||
|
||||
if (null === $data = $this->getSession()->get($this->namespace)) {
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
$filterDto
|
||||
->setDateFrom($data['date_from'])
|
||||
->setDateTo($data['date_to'])
|
||||
;
|
||||
|
||||
if (isset($data['teamer_id'])) {
|
||||
$teamer = $this
|
||||
->entityManager
|
||||
->getRepository(Teamer::class)
|
||||
->find($data['teamer_id'])
|
||||
;
|
||||
$filterDto->setTeamer($teamer);
|
||||
}
|
||||
|
||||
if (isset($data['job_profiles']) && 0 < count($data['job_profiles'])) {
|
||||
$jobProfiles = $this
|
||||
->entityManager
|
||||
->getRepository(JobProfile::class)
|
||||
->findBy(['id' => $data['job_profiles']])
|
||||
;
|
||||
$filterDto->setJobProfiles($jobProfiles);
|
||||
}
|
||||
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
protected function saveFilterSettings(AbstractFilterDto $filterDto): void
|
||||
{
|
||||
/* @var AvailabilityFilterDto $filterDto */
|
||||
$this->getSession()->set($this->namespace, [
|
||||
'date_from' => $filterDto->getDateFrom(),
|
||||
'date_to' => $filterDto->getDateTo(),
|
||||
'teamer_id' => $filterDto->getTeamer()?->getId(),
|
||||
'job_profiles' => array_map(function (JobProfile $jobProfile) {
|
||||
return $jobProfile->getId();
|
||||
}, $filterDto->getJobProfiles()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Service\Common;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use App\Model\AbstractFilterDto;
|
||||
use App\Model\FeedbackFilterDto;
|
||||
|
||||
@@ -18,6 +19,15 @@ class FeedbackFilterHandler extends AbstractFilterHandler
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
if (isset($data['teamer_id'])) {
|
||||
$teamer = $this
|
||||
->entityManager
|
||||
->getRepository(Teamer::class)
|
||||
->find($data['teamer_id'])
|
||||
;
|
||||
$filterDto->setTeamer($teamer);
|
||||
}
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$filterDto->setName($data['name']);
|
||||
}
|
||||
@@ -34,10 +44,16 @@ class FeedbackFilterHandler extends AbstractFilterHandler
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
public function applyTeamerFilter(Teamer $teamer): void
|
||||
{
|
||||
$this->saveFilterSettings((new FeedbackFilterDto())->setTeamer($teamer));
|
||||
}
|
||||
|
||||
protected function saveFilterSettings(AbstractFilterDto $filterDto): void
|
||||
{
|
||||
/* @var FeedbackFilterDto $filterDto */
|
||||
$this->getSession()->set($this->namespace, [
|
||||
'teamer_id' => $filterDto->getTeamer()?->getId(),
|
||||
'name' => $filterDto->getName(),
|
||||
'date_from' => $filterDto->getDateFrom(),
|
||||
'date_to' => $filterDto->getDateTo(),
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
namespace App\Twig\Components;
|
||||
|
||||
use App\Entity\Teamer;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent(template: 'components/rating_stars.html.twig')]
|
||||
class RatingStars
|
||||
{
|
||||
public ?int $rating;
|
||||
public ?Teamer $teamer = null;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<h2 class="text-lg font-bold pb-2">
|
||||
Bewertungsskala
|
||||
</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
5 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat hervorragende Arbeit geleistet und ist beim Team und den Gästen
|
||||
sehr gut angekommen, es gibt nichts auszusetzen.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
4 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen sehr guten Job geleistet. Wenn es etwas zu beanstanden oder
|
||||
Ideen zur Verbesserung gibt, dann sind es nur Kleinigkeiten (diese gerne in den Kommentar
|
||||
schreiben).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
3 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen ganz guten Job erledigt. Es gibt einige Dinge, die
|
||||
verbesserungswürdig sind (diese gerne in den Kommentaren vermerken). Der/die Wochenteamer:in
|
||||
ist aber nicht negativ aufgefallen.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
2 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wocheteamer:in hat keinen guten Job geleistet. Der Einsatz hat das Team eher belastet,
|
||||
als es zu unterstützen. Der/die Wochenteamer:in bekommt noch eine Chance sich zu beweisen
|
||||
(hierfür gerne ein schriftliches Feedback im Kommentarfeld hinterlassen).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
1 Punkt
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen schlechten Job erledigt, ist Team und/oder Gästen negativ
|
||||
aufgefallen und soll keine weiteren Einsätze erhalten.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -51,59 +51,7 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold pb-2">
|
||||
Bewertungsskala
|
||||
</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
5 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat hervorragende Arbeit geleistet und ist beim Team und den Gästen
|
||||
sehr gut angekommen, es gibt nichts auszusetzen.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
4 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen sehr guten Job geleistet. Wenn es etwas zu beanstanden oder
|
||||
Ideen zur Verbesserung gibt, dann sind es nur Kleinigkeiten (diese gerne in den Kommentar
|
||||
schreiben).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
3 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen ganz guten Job erledigt. Es gibt einige Dinge, die
|
||||
verbesserungswürdig sind (diese gerne in den Kommentaren vermerken). Der/die Wochenteamer:in
|
||||
ist aber nicht negativ aufgefallen.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
2 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wocheteamer:in hat keinen guten Job geleistet. Der Einsatz hat das Team eher belastet,
|
||||
als es zu unterstützen. Der/die Wochenteamer:in bekommt noch eine Chance sich zu beweisen
|
||||
(hierfür gerne ein schriftliches Feedback im Kommentarfeld hinterlassen).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
1 Punkt
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen schlechten Job erledigt, ist Team und/oder Gästen negativ
|
||||
aufgefallen und soll keine weiteren Einsätze erhalten.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% include '_partials/_feedback_rating_scale.html.twig' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -51,7 +51,7 @@
|
||||
{% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': application.teamer } %}
|
||||
{{ icon('info', 'w-4 h-4') }}
|
||||
</button>
|
||||
<twig:RatingStars rating="{{ application.teamer.averageRating }}" />
|
||||
<twig:RatingStars rating="{{ application.teamer.averageRating }}" :teamer="application.teamer" />
|
||||
</td>
|
||||
<td>
|
||||
{{ ('label.application.status.' ~ application.status)|trans }}
|
||||
@@ -162,7 +162,7 @@
|
||||
{% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': disposition.teamer } %}
|
||||
{{ icon('info', 'w-4 h-4') }}
|
||||
</button>
|
||||
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" />
|
||||
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" :teamer="disposition.teamer" />
|
||||
</td>
|
||||
<td>
|
||||
{{ ('label.disposition.status.' ~ disposition.status)|trans }}
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
<span>{{ assignment.activeDispositions|length }}/{{ assignment.availableDispositions }}</span>
|
||||
<ul>
|
||||
{% for disposition in assignment.activeDispositions %}
|
||||
<li>
|
||||
<li class="flex items-center space-x-1">
|
||||
<div role="button"
|
||||
title="Teamer:innen-Info anzeigen"
|
||||
aria-label="Teamer:innen-Info anzeigen"
|
||||
@@ -150,8 +150,8 @@
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<span class="whitespace-nowrap">{{ disposition.teamer }}</span>
|
||||
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" />
|
||||
</div>
|
||||
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" :teamer="disposition.teamer" />
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -3,9 +3,32 @@
|
||||
{% block title %}Eigene Verfügbarkeiten der Teamer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Eigene Verfügbarkeiten der Teamer
|
||||
</h1>
|
||||
<div class="flex items-start justify-between pb-4">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Eigene Verfügbarkeiten der Teamer
|
||||
</h1>
|
||||
<div class="flex flex-col items-end space-y-2 md:flex-row md:items-center md:space-x-2 md:space-y-0">
|
||||
<button type="button"
|
||||
class="{{ html_classes('btn btn--small', { 'btn--secondary': filterDto.active }) }}"
|
||||
title="Filter"
|
||||
hx-get="{{ path('app_common_availability_filter', { 'r': return_url() }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
{{ icon('filter', 'w-4 h-4 shrink-0') }}
|
||||
{% if filterDto.active %}
|
||||
<span class="whitespace-nowrap">{{ filterDto.activeFiltersCount }} Filter aktiv</span>
|
||||
{% else %}
|
||||
<span>Filter</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% if filterDto.active %}
|
||||
<a href="{{ path('app_common_availability_filter_reset', { 'r': return_url() }) }}"
|
||||
class="btn btn--small btn--secondary">
|
||||
Reset
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner">
|
||||
<table class="data-table">
|
||||
@@ -43,7 +66,7 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<td colspan="4">
|
||||
Keine Daten...
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
<div>
|
||||
{{ form_start(form) }}
|
||||
<div class="flex flex-col divide-y divide-gray-200">
|
||||
{% if form.author is defined %}
|
||||
<div class="py-4">
|
||||
{{ form_row(form.author) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for child in form.ratings.children %}
|
||||
<div class="grid grid-cols-4 gap-x-4 items-start py-4">
|
||||
<h2 class="col-span-3 text-lg font-bold">
|
||||
@@ -35,59 +40,7 @@
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold pb-2">
|
||||
Bewertungsskala
|
||||
</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
5 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat hervorragende Arbeit geleistet und ist beim Team und den Gästen
|
||||
sehr gut angekommen, es gibt nichts auszusetzen.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
4 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen sehr guten Job geleistet. Wenn es etwas zu beanstanden oder
|
||||
Ideen zur Verbesserung gibt, dann sind es nur Kleinigkeiten (diese gerne in den Kommentar
|
||||
schreiben).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
3 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen ganz guten Job erledigt. Es gibt einige Dinge, die
|
||||
verbesserungswürdig sind (diese gerne in den Kommentaren vermerken). Der/die Wochenteamer:in
|
||||
ist aber nicht negativ aufgefallen.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
2 Punkte
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wocheteamer:in hat keinen guten Job geleistet. Der Einsatz hat das Team eher belastet,
|
||||
als es zu unterstützen. Der/die Wochenteamer:in bekommt noch eine Chance sich zu beweisen
|
||||
(hierfür gerne ein schriftliches Feedback im Kommentarfeld hinterlassen).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap">
|
||||
1 Punkt
|
||||
</th>
|
||||
<td>
|
||||
Der/die Wochenteamer:in hat einen schlechten Job erledigt, ist Team und/oder Gästen negativ
|
||||
aufgefallen und soll keine weiteren Einsätze erhalten.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% include '_partials/_feedback_rating_scale.html.twig' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -74,7 +74,7 @@
|
||||
{{ icon('user', 'w-10 h-10 text-gray-200') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<twig:RatingStars rating="{{ teamer.averageRating }}" />
|
||||
<twig:RatingStars rating="{{ teamer.averageRating }}" :teamer="teamer" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
{{ icon('user', 'w-32 h-32 text-gray-200') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<twig:RatingStars rating="{{ teamer.averageRating }}"/>
|
||||
<twig:RatingStars rating="{{ teamer.averageRating }}" :teamer="teamer"/>
|
||||
<ul class="pb-8 divide-y divide-gray-200">
|
||||
{% if teamer.communication.phone %}
|
||||
<li class="py-2">
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}Verfügbarkeiten filtern{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }}
|
||||
<div class="flex flex-col space-y-2 pb-4">
|
||||
{{ form_row(filterForm.teamer) }}
|
||||
{{ form_row(filterForm.jobProfiles) }}
|
||||
{{ form_row(filterForm.dateFrom) }}
|
||||
{{ form_row(filterForm.dateTo) }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
{{ form_widget(filterForm.apply, { 'attr': { 'class': 'btn' } }) }}
|
||||
{{ form_widget(filterForm.reset, { 'attr': { 'class': 'btn btn--secondary' } }) }}
|
||||
</div>
|
||||
{{ form_rest(filterForm) }}
|
||||
{{ form_end(filterForm) }}
|
||||
{% endblock %}
|
||||
@@ -5,6 +5,7 @@
|
||||
{% block content %}
|
||||
{{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }}
|
||||
<div class="flex flex-col space-y-2 pb-4">
|
||||
{{ form_row(filterForm.teamer) }}
|
||||
{{ form_row(filterForm.name) }}
|
||||
{{ form_row(filterForm.dateFrom) }}
|
||||
{{ form_row(filterForm.dateTo) }}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<div class="inline-flex items-center">
|
||||
{% for grade in range(1, 5) %}
|
||||
{{ icon('star', html_classes('w-4 h-4', { 'text-gray-200': (rating/100)|round < grade, 'text-primary': (rating/100)|round >= grade })) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% set stars %}
|
||||
<div class="inline-flex items-center">
|
||||
{% for grade in range(1, 5) %}
|
||||
{{ icon('star', html_classes('w-4 h-4', { 'text-gray-200': (rating/100)|round < grade, 'text-primary': (rating/100)|round >= grade })) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endset %}
|
||||
{% if teamer is not null and rating and is_granted('ROLE_ADMINISTRATIVE') %}
|
||||
<a href="{{ path('app_administrative_feedback_index_teamer', { 'uuid': teamer.uuid }) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Feedback anzeigen">{{ stars }}</a>
|
||||
{% else %}
|
||||
{{ stars }}
|
||||
{% endif %}
|
||||
Reference in new issue
Block a user