WIP: Implement feedback functionality
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Entity\Feedback;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20231105160706 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\DataTransformer;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
class EntityToIdTransformer implements DataTransformerInterface
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly string $class)
|
||||
{
|
||||
}
|
||||
|
||||
public function transform($value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\DataTransformer;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
class EntityToIdentifierTransformer implements DataTransformerInterface
|
||||
{
|
||||
private string $identifier;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly string $class,
|
||||
string $identifier = 'id'
|
||||
) {
|
||||
if (false === in_array($identifier, ['id', 'uuid'])) {
|
||||
throw new \InvalidArgumentException(sprintf('Identifier must be one of "id" or "uuid" got "%s"', $identifier));
|
||||
}
|
||||
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\DataTransformer;
|
||||
|
||||
use App\Entity\FeedbackSet;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
|
||||
class FeedbackToRatingsTransformer implements DataTransformerInterface
|
||||
{
|
||||
public function __construct(private readonly FeedbackSet $feedbackSet)
|
||||
{
|
||||
}
|
||||
|
||||
public function transform(mixed $value)
|
||||
{
|
||||
// Not in use
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function reverseTransform(mixed $value)
|
||||
{
|
||||
$feedbackRatings = [];
|
||||
|
||||
foreach ($this->feedbackSet->getRatings() as $index => $rating) {
|
||||
$feedbackRatings[] = [
|
||||
'rating' => $rating,
|
||||
'mark' => $value['rating_'.$index],
|
||||
];
|
||||
}
|
||||
|
||||
return $feedbackRatings;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Entity\Feedback;
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class FeedbackVoter extends Voter
|
||||
{
|
||||
public const VIEW = 'VIEW';
|
||||
public const PUBLISH = 'PUBLISH';
|
||||
public const DELETE = 'DELETE';
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return $subject instanceof Feedback && in_array($attribute, [static::VIEW, static::PUBLISH, static::DELETE]);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
/** @var Feedback $feedback */
|
||||
$feedback = $subject;
|
||||
|
||||
if (in_array('ROLE_ADMIN', $token->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;
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,12 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="#">
|
||||
<a href="{{ path('app_admin_teamer_profile', { 'uuid': teamer.uuid }) }}">
|
||||
<span class="whitespace-nowrap">{{ teamer.lastName }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" class="whitespace-nowrap">
|
||||
<a href="{{ path('app_admin_teamer_profile', { 'uuid': teamer.uuid }) }}" class="whitespace-nowrap">
|
||||
{{ teamer.firstName }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -11,6 +11,65 @@
|
||||
<h1 class="text-2xl font-bold pb-8">
|
||||
Vergangene Einsätze & Feeback {{ teamer }}
|
||||
</h1>
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-wrapper__inner">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Einsatz
|
||||
</th>
|
||||
<th>
|
||||
Gesamtnote
|
||||
</th>
|
||||
<th>
|
||||
Kommentar
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for disposition in pagination %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ disposition.assignment.destination.dateFrom|date('d.m.Y') }}
|
||||
<br>
|
||||
{{ disposition.assignment.destination.product }}
|
||||
<br>
|
||||
{{ disposition.assignment.destination.hotel }}
|
||||
</td>
|
||||
<td>
|
||||
{% if disposition.feedback %}
|
||||
{{ disposition.feedback.averageRating }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if disposition.feedback %}
|
||||
<div class="truncate">
|
||||
{{ disposition.feedback.comment|default('-') }}
|
||||
</div>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button>
|
||||
{{ icon('feedback') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<td colspan="7">
|
||||
Keine Daten...
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ knp_pagination_render(pagination) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user