Feat: Add assignment filter for admins

This commit is contained in:
Björn Fromme
2023-10-23 15:03:17 +02:00
parent d688a05203
commit 6f7db829b5
9 changed files with 424 additions and 32 deletions
+2
View File
@@ -22,6 +22,8 @@ doctrine:
dql:
string_functions:
DATE_FORMAT: DoctrineExtensions\Query\Mysql\DateFormat
datetime_functions:
DATEDIFF: DoctrineExtensions\Query\Mysql\DateDiff
when@test:
doctrine:
dbal:
@@ -3,6 +3,7 @@
namespace App\Controller\Admin\Assignment;
use App\Repository\AssignmentRepository;
use App\Service\Common\AssignmentFilterHandler;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -14,7 +15,8 @@ class IndexController extends AbstractController
{
public function __construct(
private readonly AssignmentRepository $assignmentRepository,
private readonly PaginatorInterface $paginator
private readonly PaginatorInterface $paginator,
private readonly AssignmentFilterHandler $filterHandler
) {
}
@@ -22,9 +24,15 @@ class IndexController extends AbstractController
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): Response
{
$filterForm = $this->filterHandler->getForm();
$filterDto = $this->filterHandler->handleRequest($filterForm, $request);
if (true === $filterDto->isReset()) {
return $this->redirectToRoute('app_admin_assignment_index');
}
$query = $this
->assignmentRepository
->getListQuery()
->getListQuery($filterDto)
;
$pagination = $this->paginator->paginate(
@@ -39,6 +47,7 @@ class IndexController extends AbstractController
return $this->render('admin/assignment/index.html.twig', [
'pagination' => $pagination,
'filterForm' => $filterForm,
]);
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ class IndexController extends AbstractController
$query = $this
->assignmentRepository
->getListQuery($teamer)
->getListQueryForTeamer($teamer)
;
$pagination = $this->paginator->paginate(
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace App\Form;
use App\Entity\Destination;
use App\Entity\JobProfile;
use App\Model\AssignmentFilterDto;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AssignmentFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('dateFrom', DatepickerType::class, [
'label' => false,
'min_date' => $options['min_date'],
'max_date' => $options['max_date'],
'attr' => [
'placeholder' => 'von',
],
])
->add('dateTo', DatepickerType::class, [
'label' => false,
'min_date' => $options['min_date'],
'max_date' => $options['max_date'],
'attr' => [
'placeholder' => 'bis',
],
])
->add('duration', ChoiceType::class, [
'label' => false,
'placeholder' => 'Einsatzdauer',
'choices' => [
'Wochenende (2-4 Tage)' => AssignmentFilterDto::DURATION_WEEKEND,
'Midweek (5-6 Tage)' => AssignmentFilterDto::DURATION_MID_WEEK,
'Ganze Woche (7 Tage)' => AssignmentFilterDto::DURATION_FULL_WEEK,
'Mehr als einen Woche' => AssignmentFilterDto::DURATION_MORE,
],
])
->add('apply', SubmitType::class, [
'label' => 'anwenden',
])
->add('reset', SubmitType::class, [
'label' => 'reset',
])
;
if (0 < count($options['job_profiles'])) {
$builder
->add('jobProfile', EntityType::class, [
'label' => false,
'placeholder' => 'Job-Profil',
'class' => JobProfile::class,
'choice_label' => 'name',
'choices' => $options['job_profiles'],
])
;
}
if (0 < count($options['destinations'])) {
$builder
->add('destination', EntityType::class, [
'label' => false,
'placeholder' => 'Destination',
'class' => Destination::class,
'choice_label' => function (Destination $destination) {
return $destination->getProduct();
},
'choices' => $options['destinations'],
])
;
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => AssignmentFilterDto::class,
'min_date' => null,
'max_date' => null,
'job_profiles' => [],
'destinations' => [],
])
->setAllowedTypes('min_date', [\DateTimeImmutable::class, 'null'])
->setAllowedTypes('max_date', [\DateTimeImmutable::class, 'null'])
;
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace App\Model;
use App\Entity\Destination;
use App\Entity\JobProfile;
class AssignmentFilterDto
{
public const DURATION_WEEKEND = 1;
public const DURATION_MID_WEEK = 2;
public const DURATION_FULL_WEEK = 3;
public const DURATION_MORE = 4;
private ?\DateTimeImmutable $dateFrom = null;
private ?\DateTimeImmutable $dateTo = null;
private ?JobProfile $jobProfile = null;
private ?Destination $destination = null;
private ?int $duration = null;
private bool $reset = false;
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 getJobProfile(): ?JobProfile
{
return $this->jobProfile;
}
public function setJobProfile(?JobProfile $jobProfile): static
{
$this->jobProfile = $jobProfile;
return $this;
}
public function getDestination(): ?Destination
{
return $this->destination;
}
public function setDestination(?Destination $destination): static
{
$this->destination = $destination;
return $this;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): static
{
$this->duration = $duration;
return $this;
}
public function isReset(): bool
{
return $this->reset;
}
public function setReset(bool $reset): static
{
$this->reset = $reset;
return $this;
}
}
+83 -27
View File
@@ -4,7 +4,9 @@ namespace App\Repository;
use App\Entity\Application;
use App\Entity\Assignment;
use App\Entity\Destination;
use App\Entity\Teamer;
use App\Model\AssignmentFilterDto;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr\Join;
@@ -25,7 +27,7 @@ class AssignmentRepository extends ServiceEntityRepository
parent::__construct($registry, Assignment::class);
}
public function getListQuery(Teamer $teamer = null): Query
public function getListQuery(AssignmentFilterDto $filterDto): Query
{
$qb = $this->createQueryBuilder('assignment');
@@ -34,25 +36,81 @@ class AssignmentRepository extends ServiceEntityRepository
->innerJoin('assignment.destination', 'destination')
->innerJoin('assignment.jobProfile', 'job_profile')
->where($qb->expr()->isNull('assignment.deletedAt'))
;
if (null !== $teamer) {
$qb
->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer'))
->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer'))
->setParameter('teamer', $teamer)
->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->neq('application.status', ':status'))
->leftJoin('assignment.dispositions', 'disposition')
->setParameter('status', Application::STATUS_REJECTED)
;
} else {
if (null !== $dateFrom = $filterDto->getDateFrom()) {
$qb
->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->neq('application.status', ':status'))
->leftJoin('assignment.dispositions', 'disposition')
->setParameter('status', Application::STATUS_REJECTED)
->andWhere($qb->expr()->gte('destination.dateFrom', ':dateFrom'))
->setParameter('dateFrom', $dateFrom)
;
}
if (null !== $dateTo = $filterDto->getDateTo()) {
$qb
->andWhere($qb->expr()->lte('destination.dateTo', ':dateTo'))
->setParameter('dateTo', $dateTo)
;
}
if (null !== $jobProfile = $filterDto->getJobProfile()) {
$qb
->andWhere($qb->expr()->eq('assignment.jobProfile', ':jobProfile'))
->setParameter('jobProfile', $jobProfile)
;
}
if (null !== $destination = $filterDto->getDestination()) {
$qb
->andWhere($qb->expr()->eq('assignment.destination', ':destination'))
->setParameter('destination', $destination)
;
}
if (null !== $duration = $filterDto->getDuration()) {
$days = match ($duration) {
AssignmentFilterDto::DURATION_WEEKEND => [2, 4],
AssignmentFilterDto::DURATION_MID_WEEK => [5, 6],
AssignmentFilterDto::DURATION_FULL_WEEK => [7, 7],
default => [8, 0],
};
[$minDays, $maxDays] = $days;
$qb
->andWhere($qb->expr()->gte('DATEDIFF(destination.dateTo, destination.dateFrom)', ':minDays'))
->setParameter('minDays', $minDays)
;
if (0 < $maxDays) {
$qb
->andWhere($qb->expr()->lte('DATEDIFF(destination.dateTo, destination.dateFrom)', ':maxDays'))
->setParameter('maxDays', $maxDays)
;
}
}
return $qb->getQuery();
}
public function getListQueryForTeamer(Teamer $teamer = null): Query
{
$qb = $this->createQueryBuilder('assignment');
return $qb
->select('assignment', 'destination', 'job_profile', 'application', 'disposition')
->innerJoin('assignment.destination', 'destination')
->innerJoin('assignment.jobProfile', 'job_profile')
->where($qb->expr()->isNull('assignment.deletedAt'))
->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer'))
->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer'))
->setParameter('teamer', $teamer)
->getQuery()
;
}
public function getBookmarkQueryForTeamer(Teamer $teamer): Query
{
$qb = $this->createQueryBuilder('assignment');
@@ -85,39 +143,37 @@ class AssignmentRepository extends ServiceEntityRepository
->getSingleResult()
;
$options['minDate'] = $result['minDate'];
$options['maxDate'] = $result['maxDate'];
$options['minDate'] = new \DateTimeImmutable($result['minDate']);
$options['maxDate'] = new \DateTimeImmutable($result['maxDate']);
// Job-profiles
$qb = $this->createQueryBuilder('assignment');
$result = $qb
->select('job_profile.id', 'job_profile.name')
->select('assignment', 'job_profile')
->innerJoin('assignment.jobProfile', 'job_profile')
->orderBy('job_profile.name', 'ASC')
->where($qb->expr()->isNull('assignment.deletedAt'))
->getQuery()
->getArrayResult()
->getResult()
;
$options['jobProfiles'] = array_map(function (Assignment $assignment) {
return $assignment->getJobProfile();
}, $result);
foreach ($result as $row) {
$options['jobProfiles'][$row['id']] = $row['name'];
}
// Products
// Destinations
$qb = $this->createQueryBuilder('assignment');
$result = $qb
->select('destination.id id', 'destination.product product', 'destination.hotel hotel')
->select('assignment', 'destination')
->innerJoin('assignment.destination', 'destination')
->where($qb->expr()->isNull('assignment.deletedAt'))
->orderBy('destination.product', 'ASC')
->groupBy('destination.id')
->getQuery()
->getArrayResult()
->getResult()
;
foreach ($result as $row) {
$options['products'][$row['id']] = $row['product'];
}
$options['destinations'] = array_map(function (Assignment $assignment) {
return $assignment->getDestination();
}, $result);
return $options;
}
@@ -0,0 +1,116 @@
<?php
namespace App\Service\Common;
use App\Entity\Assignment;
use App\Entity\Destination;
use App\Entity\JobProfile;
use App\Form\AssignmentFilterType;
use App\Model\AssignmentFilterDto;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class AssignmentFilterHandler
{
public function __construct(
private readonly RequestStack $requestStack,
private readonly EntityManagerInterface $entityManager,
private readonly FormFactoryInterface $formFactory,
) {
}
public function getForm(): FormInterface
{
$filterDto = $this->getFilterSettings();
$assignmentRepository = $this->entityManager->getRepository(Assignment::class);
$filterOptions = $assignmentRepository->getFilterOptions();
return $this->formFactory->create(AssignmentFilterType::class, $filterDto, [
'min_date' => $filterOptions['minDate'],
'max_date' => $filterOptions['maxDate'],
'job_profiles' => $filterOptions['jobProfiles'],
'destinations' => $filterOptions['destinations'],
]);
}
public function handleRequest(FormInterface $form, Request $request): AssignmentFilterDto
{
$form->handleRequest($request);
$filterDto = $form->getData();
if ($form->has('reset') && $form->get('reset')->isClicked()) {
$filterDto = new AssignmentFilterDto();
$filterDto->setReset(true);
$this->getSession()->remove('assignment_filter');
} elseif ($form->has('apply') && $form->get('apply')->isClicked()) {
$this->saveFilterSettings($filterDto);
}
return $filterDto;
}
public function getFilterSettings(): AssignmentFilterDto
{
if (null === $this->getSession()->get('assignment_filter')) {
return new AssignmentFilterDto();
}
return $this->loadFilterSettings();
}
public function loadFilterSettings(): AssignmentFilterDto
{
$data = $this->getSession()->get('assignment_filter');
$filterDto = new AssignmentFilterDto();
if (isset($data['date_from'])) {
$filterDto->setDateFrom(new \DateTimeImmutable($data['date_from']));
}
if (isset($data['date_to'])) {
$filterDto->setDateTo(new \DateTimeImmutable($data['date_to']));
}
if (isset($data['job_profile']) && 0 < (int) $data['job_profile']) {
$jobProfile = $this
->entityManager
->getRepository(JobProfile::class)
->find($data['job_profile'])
;
$filterDto->setJobProfile($jobProfile);
}
if (isset($data['destination']) && 0 < (int) $data['destination']) {
$destination = $this
->entityManager
->getRepository(Destination::class)
->find($data['destination'])
;
$filterDto->setDestination($destination);
}
if (isset($data['duration']) && 0 < (int) $data['duration']) {
$filterDto->setDuration($data['duration']);
}
return $filterDto;
}
public function saveFilterSettings(AssignmentFilterDto $filterDto): void
{
$this->getSession()->set('assignment_filter', [
'date_from' => $filterDto->getDateFrom()?->format('Y-m-d'),
'date_to' => $filterDto->getDateTo()?->format('Y-m-d'),
'job_profile' => $filterDto->getJobProfile()?->getId(),
'destination' => $filterDto->getDestination()?->getId(),
'duration' => $filterDto->getDuration(),
]);
}
private function getSession(): SessionInterface
{
return $this->requestStack->getSession();
}
}
+23 -1
View File
@@ -3,9 +3,25 @@
{% block title %}Einsatzübersicht{% endblock %}
{% block content %}
<h1 class="text-2xl font-bold pb-8">
<h1 class="text-2xl font-bold pb-4">
Einsatzübersicht
</h1>
{{ form_start(filterForm) }}
<div class="border border-gray-200 rounded-md p-2 mb-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-1 pb-2">
{{ form_widget(filterForm.dateFrom) }}
{{ form_widget(filterForm.dateTo) }}
{{ form_widget(filterForm.jobProfile) }}
{{ form_widget(filterForm.destination) }}
{{ form_widget(filterForm.duration) }}
</div>
<div class="flex items-center space-x-2">
{{ form_widget(filterForm.apply, { 'attr': { 'class': 'btn btn--small' } }) }}
{{ form_widget(filterForm.reset, { 'attr': { 'class': 'btn btn--small btn--secondary' } }) }}
</div>
</div>
{{ form_rest(filterForm) }}
{{ form_end(filterForm) }}
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
<table class="data-table">
@@ -88,6 +104,12 @@
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6">
Keine Daten...
</td>
</tr>
{% endfor %}
</tbody>
</table>
+1 -1
View File
@@ -164,7 +164,7 @@
{%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%}
{%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%}
{%- if errors|length -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder:red-500 focus:ring-red-500' }) -%}
{% else %}
{%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%}
{%- endif -%}