WIP: Implement feedback functionality
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user