feat: link ratings widget with feedback list filtered by teamer

addresses #869at5mdu
This commit is contained in:
Björn Fromme
2026-08-24 13:50:07 +02:00
parent 43968e4e7b
commit dfa3e63926
12 changed files with 83 additions and 11 deletions
@@ -2,6 +2,7 @@
namespace App\Controller\Administrative\Feedback; namespace App\Controller\Administrative\Feedback;
use App\Entity\Teamer;
use App\Repository\FeedbackRepository; use App\Repository\FeedbackRepository;
use App\Service\Common\FeedbackFilterHandler; use App\Service\Common\FeedbackFilterHandler;
use Knp\Component\Pager\PaginatorInterface; use Knp\Component\Pager\PaginatorInterface;
@@ -46,4 +47,15 @@ class IndexController extends AbstractController
'filterDto' => $filterDto, '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');
}
} }
+9
View File
@@ -2,6 +2,7 @@
namespace App\Form; namespace App\Form;
use App\Entity\Teamer;
use App\Model\FeedbackFilterDto; use App\Model\FeedbackFilterDto;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -15,6 +16,14 @@ class FeedbackFilterType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $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, [ ->add('name', TextType::class, [
'label' => 'Name', 'label' => 'Name',
'required' => false, 'required' => false,
+15
View File
@@ -2,13 +2,28 @@
namespace App\Model; namespace App\Model;
use App\Entity\Teamer;
class FeedbackFilterDto extends AbstractFilterDto class FeedbackFilterDto extends AbstractFilterDto
{ {
protected ?Teamer $teamer = null;
protected ?string $name = null; protected ?string $name = null;
protected ?\DateTimeImmutable $dateFrom = null; protected ?\DateTimeImmutable $dateFrom = null;
protected ?\DateTimeImmutable $dateTo = null; protected ?\DateTimeImmutable $dateTo = null;
protected ?int $averageRating = 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 public function getName(): ?string
{ {
return $this->name; return $this->name;
+7
View File
@@ -38,6 +38,13 @@ class FeedbackRepository extends ServiceEntityRepository
->innerJoin('teamer.user', 'user') ->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()) { if (null !== $name = $filterDto->getName()) {
$qb $qb
->andWhere($qb->expr()->orX( ->andWhere($qb->expr()->orX(
@@ -2,6 +2,7 @@
namespace App\Service\Common; namespace App\Service\Common;
use App\Entity\Teamer;
use App\Model\AbstractFilterDto; use App\Model\AbstractFilterDto;
use App\Model\FeedbackFilterDto; use App\Model\FeedbackFilterDto;
@@ -18,6 +19,15 @@ class FeedbackFilterHandler extends AbstractFilterHandler
return $filterDto; return $filterDto;
} }
if (isset($data['teamer_id'])) {
$teamer = $this
->entityManager
->getRepository(Teamer::class)
->find($data['teamer_id'])
;
$filterDto->setTeamer($teamer);
}
if (isset($data['name'])) { if (isset($data['name'])) {
$filterDto->setName($data['name']); $filterDto->setName($data['name']);
} }
@@ -34,10 +44,16 @@ class FeedbackFilterHandler extends AbstractFilterHandler
return $filterDto; return $filterDto;
} }
public function applyTeamerFilter(Teamer $teamer): void
{
$this->saveFilterSettings((new FeedbackFilterDto())->setTeamer($teamer));
}
protected function saveFilterSettings(AbstractFilterDto $filterDto): void protected function saveFilterSettings(AbstractFilterDto $filterDto): void
{ {
/* @var FeedbackFilterDto $filterDto */ /* @var FeedbackFilterDto $filterDto */
$this->getSession()->set($this->namespace, [ $this->getSession()->set($this->namespace, [
'teamer_id' => $filterDto->getTeamer()?->getId(),
'name' => $filterDto->getName(), 'name' => $filterDto->getName(),
'date_from' => $filterDto->getDateFrom(), 'date_from' => $filterDto->getDateFrom(),
'date_to' => $filterDto->getDateTo(), 'date_to' => $filterDto->getDateTo(),
+2
View File
@@ -2,10 +2,12 @@
namespace App\Twig\Components; namespace App\Twig\Components;
use App\Entity\Teamer;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent(template: 'components/rating_stars.html.twig')] #[AsTwigComponent(template: 'components/rating_stars.html.twig')]
class RatingStars class RatingStars
{ {
public ?int $rating; public ?int $rating;
public ?Teamer $teamer = null;
} }
@@ -51,7 +51,7 @@
{% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': application.teamer } %} {% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': application.teamer } %}
{{ icon('info', 'w-4 h-4') }} {{ icon('info', 'w-4 h-4') }}
</button> </button>
<twig:RatingStars rating="{{ application.teamer.averageRating }}" /> <twig:RatingStars rating="{{ application.teamer.averageRating }}" :teamer="application.teamer" />
</td> </td>
<td> <td>
{{ ('label.application.status.' ~ application.status)|trans }} {{ ('label.application.status.' ~ application.status)|trans }}
@@ -162,7 +162,7 @@
{% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': disposition.teamer } %} {% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': disposition.teamer } %}
{{ icon('info', 'w-4 h-4') }} {{ icon('info', 'w-4 h-4') }}
</button> </button>
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" /> <twig:RatingStars rating="{{ disposition.teamer.averageRating }}" :teamer="disposition.teamer" />
</td> </td>
<td> <td>
{{ ('label.disposition.status.' ~ disposition.status)|trans }} {{ ('label.disposition.status.' ~ disposition.status)|trans }}
@@ -142,7 +142,7 @@
<span>{{ assignment.activeDispositions|length }}/{{ assignment.availableDispositions }}</span> <span>{{ assignment.activeDispositions|length }}/{{ assignment.availableDispositions }}</span>
<ul> <ul>
{% for disposition in assignment.activeDispositions %} {% for disposition in assignment.activeDispositions %}
<li> <li class="flex items-center space-x-1">
<div role="button" <div role="button"
title="Teamer:innen-Info anzeigen" title="Teamer:innen-Info anzeigen"
aria-label="Teamer:innen-Info anzeigen" aria-label="Teamer:innen-Info anzeigen"
@@ -150,8 +150,8 @@
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
<span class="whitespace-nowrap">{{ disposition.teamer }}</span> <span class="whitespace-nowrap">{{ disposition.teamer }}</span>
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" />
</div> </div>
<twig:RatingStars rating="{{ disposition.teamer.averageRating }}" :teamer="disposition.teamer" />
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@@ -74,7 +74,7 @@
{{ icon('user', 'w-10 h-10 text-gray-200') }} {{ icon('user', 'w-10 h-10 text-gray-200') }}
</div> </div>
{% endif %} {% endif %}
<twig:RatingStars rating="{{ teamer.averageRating }}" /> <twig:RatingStars rating="{{ teamer.averageRating }}" :teamer="teamer" />
</div> </div>
</td> </td>
<td> <td>
@@ -63,7 +63,7 @@
{{ icon('user', 'w-32 h-32 text-gray-200') }} {{ icon('user', 'w-32 h-32 text-gray-200') }}
</div> </div>
{% endif %} {% endif %}
<twig:RatingStars rating="{{ teamer.averageRating }}"/> <twig:RatingStars rating="{{ teamer.averageRating }}" :teamer="teamer"/>
<ul class="pb-8 divide-y divide-gray-200"> <ul class="pb-8 divide-y divide-gray-200">
{% if teamer.communication.phone %} {% if teamer.communication.phone %}
<li class="py-2"> <li class="py-2">
@@ -5,6 +5,7 @@
{% block content %} {% block content %}
{{ form_start(filterForm, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }} {{ 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"> <div class="flex flex-col space-y-2 pb-4">
{{ form_row(filterForm.teamer) }}
{{ form_row(filterForm.name) }} {{ form_row(filterForm.name) }}
{{ form_row(filterForm.dateFrom) }} {{ form_row(filterForm.dateFrom) }}
{{ form_row(filterForm.dateTo) }} {{ form_row(filterForm.dateTo) }}
+15 -5
View File
@@ -1,5 +1,15 @@
<div class="inline-flex items-center"> {% set stars %}
{% for grade in range(1, 5) %} <div class="inline-flex items-center">
{{ icon('star', html_classes('w-4 h-4', { 'text-gray-200': (rating/100)|round < grade, 'text-primary': (rating/100)|round >= grade })) }} {% for grade in range(1, 5) %}
{% endfor %} {{ icon('star', html_classes('w-4 h-4', { 'text-gray-200': (rating/100)|round < grade, 'text-primary': (rating/100)|round >= grade })) }}
</div> {% 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 %}