feat: selectable feedback author for administrative users

addresses #869at5nbp
This commit is contained in:
Björn Fromme
2026-08-24 12:46:19 +02:00
parent 4ae23bf8a1
commit 43968e4e7b
9 changed files with 218 additions and 179 deletions
@@ -37,8 +37,8 @@ class ProvideController extends AbstractController
// Feedback provided by admins is published instantly // Feedback provided by admins is published instantly
$feedback->setStatus(Feedback::STATUS_PUBLISHED); $feedback->setStatus(Feedback::STATUS_PUBLISHED);
if (null !== $form->get('authorOverride')->getData()) { if (null !== $author = $form->get('authorOverride')->getData()) {
$feedback->setAuthor($form->get('authorOverride')->getData()); $feedback->setAuthor($author->getFullName());
} }
$this->entityManager->persist($feedback); $this->entityManager->persist($feedback);
@@ -56,6 +56,12 @@ class ProvideController extends AbstractController
$feedback->setStatus(Feedback::STATUS_PUBLISHED); $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 = $disposition->getTeamer();
$teamer->addFeedback($feedback); $teamer->addFeedback($feedback);
$disposition->setFeedback($feedback); $disposition->setFeedback($feedback);
+51
View File
@@ -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,
]);
}
}
+18 -43
View File
@@ -2,21 +2,17 @@
namespace App\Form; namespace App\Form;
use App\Entity\Feedback;
use App\Entity\FeedbackSet; use App\Entity\FeedbackSet;
use App\Entity\Teamer; use App\Entity\Teamer;
use App\Entity\User; use App\Entity\User;
use App\Repository\FeedbackSetRepository; use App\Repository\FeedbackSetRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; 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\FormBuilderInterface;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
class AdminFeedbackType extends AbstractType class AdminFeedbackType extends AbstractFeedbackType
{ {
public function __construct(private readonly FeedbackSetRepository $feedbackSetRepository) public function __construct(private readonly FeedbackSetRepository $feedbackSetRepository)
{ {
@@ -34,7 +30,7 @@ class AdminFeedbackType extends AbstractType
]) ])
->add('authorOverride', AutocompleteEntityType::class, [ ->add('authorOverride', AutocompleteEntityType::class, [
'mapped' => false, 'mapped' => false,
'label' => 'abweichender Absender', 'label' => 'Feedbackgeber:in',
'class' => User::class, 'class' => User::class,
'label_property' => 'lastName', 'label_property' => 'lastName',
'label_function' => fn (User $user) => (string) $user, 'label_function' => fn (User $user) => (string) $user,
@@ -57,7 +53,7 @@ class AdminFeedbackType extends AbstractType
return; return;
} }
$this->addDynamicForm($feedbackSet, $form); $this->addFeedbackFields($form, $feedbackSet);
}) })
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData(); $data = $event->getData();
@@ -67,47 +63,26 @@ class AdminFeedbackType extends AbstractType
return; return;
} }
$feedbackSet = $this->feedbackSetRepository->find($data['feedbackSet']); if (null === $feedbackSet = $this->feedbackSetRepository->find($data['feedbackSet'])) {
$this->addDynamicForm($feedbackSet, $form); return;
}) }
;
}
private function addDynamicForm(FeedbackSet $feedbackSet, FormInterface $form): void $this->addFeedbackFields($form, $feedbackSet);
{ })
$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 public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver parent::configureOptions($resolver);
->setDefaults([
'data_class' => Feedback::class, $resolver->setDefaults([
'validation_groups' => [ // the disposition-driven flow guarantees teamer and feedback set structurally, here
'Default', // they are picked by hand and must be validated
'admin', 'validation_groups' => [
], 'Default',
]) 'admin',
; ],
]);
} }
} }
+52 -28
View File
@@ -2,46 +2,70 @@
namespace App\Form; namespace App\Form;
use App\Entity\Feedback;
use App\Entity\FeedbackSet; use App\Entity\FeedbackSet;
use Symfony\Component\Form\AbstractType; use App\Entity\User;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use App\Repository\UserRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; 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 public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
->add('ratings', FeedbackRatingsType::class, [ $this->addAuthorField($builder);
'label' => false, }
'feedback_set' => $options['feedback_set'],
]) $this->addFeedbackFields($builder, $options['feedback_set']);
->add('comment', TextareaType::class, [ }
'label' => 'Kommentar (öffentlich)',
'required' => false, /**
'attr' => [ * Feedback::$author is a name snapshot rather than a relation, so the field stays unmapped and
'rows' => 5, * the controller writes the chosen user's full name.
], */
'help' => 'Bitte formuliere deinen Kommentar so, dass er an das Team weitergegeben werden kann', private function addAuthorField(FormBuilderInterface $builder): void
]) {
->add('commentInternal', TextareaType::class, [ $roles = array_diff(array_keys(User::ROLES), ['ROLE_TEAMER']);
'label' => 'Kommentar (intern)',
'required' => false, $choices = $this->userRepository->getUsersByRoles($roles);
'attr' => [
'rows' => 5, /** @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 public function configureOptions(OptionsResolver $resolver): void
{ {
parent::configureOptions($resolver);
$resolver $resolver
->setDefaults([
'data_class' => Feedback::class,
])
->setRequired(['feedback_set']) ->setRequired(['feedback_set'])
->setAllowedTypes('feedback_set', FeedbackSet::class) ->setAllowedTypes('feedback_set', FeedbackSet::class)
; ;
+29
View File
@@ -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 * @param string[]|string $hotelCode
* *
@@ -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>
+1 -53
View File
@@ -51,59 +51,7 @@
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
<div> <div>
<h2 class="text-lg font-bold pb-2"> {% include '_partials/_feedback_rating_scale.html.twig' %}
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>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -13,6 +13,11 @@
<div> <div>
{{ form_start(form) }} {{ form_start(form) }}
<div class="flex flex-col divide-y divide-gray-200"> <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 %} {% for child in form.ratings.children %}
<div class="grid grid-cols-4 gap-x-4 items-start py-4"> <div class="grid grid-cols-4 gap-x-4 items-start py-4">
<h2 class="col-span-3 text-lg font-bold"> <h2 class="col-span-3 text-lg font-bold">
@@ -35,59 +40,7 @@
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
<div> <div>
<h2 class="text-lg font-bold pb-2"> {% include '_partials/_feedback_rating_scale.html.twig' %}
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>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}