feat: review received form submissions in the admin ui
This commit is contained in:
1 parent
f073a2014d
commit
cb2d64f2cd
19 files changed
+999
-7
No files matched your search
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Admin\ApplicationFormSubmission;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\ApplicationFormSubmission;
|
||||
use App\Form\ApplicationFormSubmissionCallsType;
|
||||
use App\Model\ApplicationFormSubmissionCallsDto;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Saves the two "call held" checkboxes of the detail view. Rendered by DetailController, which
|
||||
* builds the same form - this one only takes the submission.
|
||||
*/
|
||||
class CallsController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/application-form-submission/calls/{uuid}', name: 'app_admin_application_form_submission_calls', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function index(ApplicationFormSubmission $submission, Request $request): Response
|
||||
{
|
||||
$callsDto = new ApplicationFormSubmissionCallsDto($submission);
|
||||
$form = $this->createForm(ApplicationFormSubmissionCallsType::class, $callsDto);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$callsDto->applyToSubmission();
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Update application form submission calls', [
|
||||
'uuid' => $submission->getUuid(),
|
||||
'firstCallHeldAt' => $submission->getFirstCallHeldAt()?->format('Y-m-d'),
|
||||
'secondCallHeldAt' => $submission->getSecondCallHeldAt()?->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Die Calls wurden gespeichert');
|
||||
}
|
||||
|
||||
return $this->redirect($this->detailUrl($submission, $request));
|
||||
}
|
||||
|
||||
private function detailUrl(ApplicationFormSubmission $submission, Request $request): string
|
||||
{
|
||||
return $this->generateUrl('app_admin_application_form_submission_detail', [
|
||||
'uuid' => $submission->getUuid(),
|
||||
'r' => $request->query->get('r'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Admin\ApplicationFormSubmission;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\ApplicationFormSubmission;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* A real DELETE, not setDeleted(): the retention rule for job applications asks for the row to
|
||||
* be gone, and "details" and "payload" go with it. The audit log keeps the uuid and who deleted
|
||||
* it, deliberately without the applicant's data.
|
||||
*/
|
||||
class DeleteController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/application-form-submission/delete/{uuid}', name: 'app_admin_application_form_submission_delete')]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function index(ApplicationFormSubmission $submission, Request $request): Response
|
||||
{
|
||||
if (true === $request->isMethod(Request::METHOD_POST)) {
|
||||
$uuid = $submission->getUuid();
|
||||
|
||||
$this->entityManager->remove($submission);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Delete application form submission', [
|
||||
'uuid' => $uuid,
|
||||
'form' => $submission->getForm(),
|
||||
'submittedAt' => $submission->getSubmittedAt()->format(\DateTimeInterface::ATOM),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Das eingegangene Formular wurde gelöscht');
|
||||
|
||||
return new HxRedirectResponse(
|
||||
$this->getReturnUrl($request, 'app_admin_application_form_submission_index')
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('admin/application_form_submission/modal_delete.html.twig', [
|
||||
'submission' => $submission,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Admin\ApplicationFormSubmission;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\ApplicationFormSubmission;
|
||||
use App\Form\ApplicationFormSubmissionCallsType;
|
||||
use App\Model\ApplicationFormSubmissionCallsDto;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class DetailController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
#[Route('/admin/application-form-submission/detail/{uuid}', name: 'app_admin_application_form_submission_detail')]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function index(ApplicationFormSubmission $submission, Request $request): Response
|
||||
{
|
||||
// Rendered here, taken by CallsController - the form has no validation to fail, so it
|
||||
// never has to come back to this page with errors.
|
||||
$callsForm = $this->createForm(
|
||||
ApplicationFormSubmissionCallsType::class,
|
||||
new ApplicationFormSubmissionCallsDto($submission),
|
||||
['action' => $this->generateUrl('app_admin_application_form_submission_calls', [
|
||||
'uuid' => $submission->getUuid(),
|
||||
'r' => $request->query->get('r'),
|
||||
])]
|
||||
);
|
||||
|
||||
return $this->render('admin/application_form_submission/detail.html.twig', [
|
||||
'submission' => $submission,
|
||||
'callsForm' => $callsForm->createView(),
|
||||
'returnUrl' => $this->getReturnUrl($request, 'app_admin_application_form_submission_index'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Admin\ApplicationFormSubmission;
|
||||
|
||||
use App\Repository\ApplicationFormSubmissionRepository;
|
||||
use App\Service\Common\ApplicationFormSubmissionFilterHandler;
|
||||
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\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* The forms the public websites posted here, newest first. Read-only apart from the follow-up
|
||||
* dates and the deletion the retention rule asks for - nothing in this application creates a
|
||||
* submission.
|
||||
*/
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApplicationFormSubmissionRepository $submissionRepository,
|
||||
private readonly PaginatorInterface $paginator,
|
||||
private readonly ApplicationFormSubmissionFilterHandler $filterHandler,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/admin/application-form-submission', name: 'app_admin_application_form_submission_index')]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$filterDto = $this->filterHandler->getFilterSettings();
|
||||
|
||||
$pagination = $this->paginator->paginate(
|
||||
$this->submissionRepository->getListQuery($filterDto),
|
||||
$request->query->getInt('page', 1),
|
||||
$request->query->getInt('limit', 50),
|
||||
[
|
||||
'defaultSortFieldName' => 'submission.submittedAt',
|
||||
'defaultSortDirection' => 'desc',
|
||||
]
|
||||
);
|
||||
|
||||
return $this->render('admin/application_form_submission/index.html.twig', [
|
||||
'pagination' => $pagination,
|
||||
'filterDto' => $filterDto,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Common;
|
||||
|
||||
use App\Form\ApplicationFormSubmissionFilterType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Service\Common\ApplicationFormSubmissionFilterHandler;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class ApplicationFormSubmissionFilterController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly ApplicationFormSubmissionFilterHandler $filterHandler)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/common/application-form-submission/filter', name: 'app_common_application_form_submission_filter')]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$formData = $this->filterHandler->getFilterSettings();
|
||||
$form = $this->createForm(ApplicationFormSubmissionFilterType::class, $formData);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->filterHandler->handleRequest($form);
|
||||
|
||||
return new HxRedirectResponse(
|
||||
$this->generateUrl('app_admin_application_form_submission_index')
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('common/modal_application_form_submission_filter.html.twig', [
|
||||
'filterForm' => $form->createView(),
|
||||
'filterDto' => $form->getData(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/common/application-form-submission/filter/reset', name: 'app_common_application_form_submission_filter_reset')]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function reset(): Response
|
||||
{
|
||||
$this->filterHandler->resetFilterSettings();
|
||||
|
||||
return $this->redirectToRoute('app_admin_application_form_submission_index');
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,13 @@ use Symfony\Component\Uid\Uuid;
|
||||
* first, other "collect it and let a human read it" forms later. Unrelated to
|
||||
* App\Entity\Application, which is a teamer applying for an assignment.
|
||||
*
|
||||
* Deliberately neither Blameable nor SoftDeletable: nothing in this application creates these
|
||||
* rows, and the retention rule for job applications asks for real deletion, so this is the one
|
||||
* entity here that gets an actual DELETE. A purge has to cover "details" and "payload" as
|
||||
* well - a json column is exactly what a deletion job forgets.
|
||||
* Deliberately neither Blameable nor SoftDeletable. Nothing here creates a row - TYPO3 does -
|
||||
* and the only thing staff write are the four follow-up dates, where the interesting fact is
|
||||
* that the mail went out or the call happened, not who ticked it off; who did is in the audit
|
||||
* log the mutating controllers write. No SoftDeletable because the retention rule for job
|
||||
* applications asks for real deletion, so this is the one entity here that gets an actual
|
||||
* DELETE. A purge has to cover "details" and "payload" as well - a json column is exactly what
|
||||
* a deletion job forgets.
|
||||
*
|
||||
* The typed columns are the mapped core agreed with the TYPO3 side and all nullable: a form is
|
||||
* configured without a field as often as with it. "details" is everything else the visitor
|
||||
@@ -86,14 +89,36 @@ class ApplicationFormSubmission implements TimestampableEntityInterface
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $city = null;
|
||||
|
||||
/**
|
||||
* The follow-up a reviewer works through after a submission arrived: two mails and two
|
||||
* phone calls, each stamped when it happened and null until it has. Staff state, never part
|
||||
* of the payload - fromDto() must not touch them.
|
||||
*
|
||||
* The mails carry a time because a send time is a machine fact and stays one once the
|
||||
* notification mail of §6 is automated. A held call does not: the reviewer records it
|
||||
* afterwards and knows the day, so storing an invented time would only look precise.
|
||||
*/
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $firstMailSentAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $secondMailSentAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $firstCallHeldAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $secondCallHeldAt = null;
|
||||
|
||||
/**
|
||||
* Everything the visitor filled in beyond the mapped core, keyed by the German field label.
|
||||
* Display only: the labels are staff-written and change on a reword, so nothing may query
|
||||
* this, build a column from it or migrate old rows to a new spelling (§3.3).
|
||||
*
|
||||
* Note the labels come back in MySQL's order, not the form's - a json column normalises an
|
||||
* object by sorting its keys by length. Render it in whatever order suits the reviewer; the
|
||||
* form's own order is still in $payload, which is read as one blob and never sorted by us.
|
||||
* The key order is whatever the engine hands back and is not part of what this carries: on
|
||||
* MariaDB (dev and production) JSON is an alias for LONGTEXT and the document survives
|
||||
* verbatim, on MySQL 8 an object is normalised and its keys come back sorted by length.
|
||||
* Render it in whatever order suits the reviewer rather than relying on either.
|
||||
*
|
||||
* @var array<string, string|bool|list<string>>
|
||||
*/
|
||||
@@ -309,6 +334,54 @@ class ApplicationFormSubmission implements TimestampableEntityInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFirstMailSentAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->firstMailSentAt;
|
||||
}
|
||||
|
||||
public function setFirstMailSentAt(?\DateTimeImmutable $firstMailSentAt): static
|
||||
{
|
||||
$this->firstMailSentAt = $firstMailSentAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecondMailSentAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->secondMailSentAt;
|
||||
}
|
||||
|
||||
public function setSecondMailSentAt(?\DateTimeImmutable $secondMailSentAt): static
|
||||
{
|
||||
$this->secondMailSentAt = $secondMailSentAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFirstCallHeldAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->firstCallHeldAt;
|
||||
}
|
||||
|
||||
public function setFirstCallHeldAt(?\DateTimeImmutable $firstCallHeldAt): static
|
||||
{
|
||||
$this->firstCallHeldAt = $firstCallHeldAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecondCallHeldAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->secondCallHeldAt;
|
||||
}
|
||||
|
||||
public function setSecondCallHeldAt(?\DateTimeImmutable $secondCallHeldAt): static
|
||||
{
|
||||
$this->secondCallHeldAt = $secondCallHeldAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool|list<string>>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Model\ApplicationFormSubmissionCallsDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ApplicationFormSubmissionCallsType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('firstCallHeld', CheckboxType::class, [
|
||||
'label' => '1. Call geführt',
|
||||
'required' => false,
|
||||
])
|
||||
->add('secondCallHeld', CheckboxType::class, [
|
||||
'label' => '2. Call geführt',
|
||||
'required' => false,
|
||||
])
|
||||
->add('save', SubmitType::class, [
|
||||
'label' => 'speichern',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => ApplicationFormSubmissionCallsDto::class,
|
||||
// The DTO is built from the submission and has no no-arg constructor.
|
||||
'empty_data' => null,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Model\ApplicationFormSubmissionFilterDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ApplicationFormSubmissionFilterType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Name',
|
||||
'required' => false,
|
||||
'help' => 'Vor- oder Nachname, Teil genügt',
|
||||
])
|
||||
->add('dateFrom', DatepickerType::class, [
|
||||
'label' => 'eingegangen von',
|
||||
'required' => false,
|
||||
])
|
||||
->add('dateTo', DatepickerType::class, [
|
||||
'label' => 'eingegangen bis',
|
||||
'required' => false,
|
||||
])
|
||||
->add('apply', SubmitType::class, [
|
||||
'label' => 'filtern',
|
||||
])
|
||||
->add('reset', SubmitType::class, [
|
||||
'label' => 'reset',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => ApplicationFormSubmissionFilterDto::class,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,18 @@ class AdminMenuBuilder extends AbstractMenuBuilder
|
||||
'icon' => 'mail',
|
||||
],
|
||||
]);
|
||||
$menu->addChild('Eingegangene Bewerbungen', [
|
||||
'route' => 'app_admin_application_form_submission_index',
|
||||
'linkAttributes' => [
|
||||
'title' => 'Eingegangene Bewerbungen',
|
||||
],
|
||||
'extras' => [
|
||||
'icon' => 'file',
|
||||
'routes' => [
|
||||
['pattern' => '/^app_admin_application_form_submission_/'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$documentMenu = $menu->addChild('Dokumente', [
|
||||
'extras' => [
|
||||
'icon' => 'document',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\ApplicationFormSubmission;
|
||||
|
||||
/**
|
||||
* The two "call held" checkboxes of the detail view. The entity stores the day each call
|
||||
* happened, the reviewer only ticks a box - this is the translation between the two.
|
||||
*
|
||||
* Unticking clears the date. Ticking a box whose date is already set leaves that date alone, so
|
||||
* saving the form again does not silently move a call from the day it happened to today.
|
||||
*/
|
||||
class ApplicationFormSubmissionCallsDto
|
||||
{
|
||||
private bool $firstCallHeld;
|
||||
|
||||
private bool $secondCallHeld;
|
||||
|
||||
public function __construct(private readonly ApplicationFormSubmission $submission)
|
||||
{
|
||||
$this->firstCallHeld = null !== $submission->getFirstCallHeldAt();
|
||||
$this->secondCallHeld = null !== $submission->getSecondCallHeldAt();
|
||||
}
|
||||
|
||||
public function isFirstCallHeld(): bool
|
||||
{
|
||||
return $this->firstCallHeld;
|
||||
}
|
||||
|
||||
public function setFirstCallHeld(bool $firstCallHeld): static
|
||||
{
|
||||
$this->firstCallHeld = $firstCallHeld;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isSecondCallHeld(): bool
|
||||
{
|
||||
return $this->secondCallHeld;
|
||||
}
|
||||
|
||||
public function setSecondCallHeld(bool $secondCallHeld): static
|
||||
{
|
||||
$this->secondCallHeld = $secondCallHeld;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function applyToSubmission(): void
|
||||
{
|
||||
$this->submission
|
||||
->setFirstCallHeldAt($this->resolve($this->firstCallHeld, $this->submission->getFirstCallHeldAt()))
|
||||
->setSecondCallHeldAt($this->resolve($this->secondCallHeld, $this->submission->getSecondCallHeldAt()))
|
||||
;
|
||||
}
|
||||
|
||||
private function resolve(bool $held, ?\DateTimeImmutable $current): ?\DateTimeImmutable
|
||||
{
|
||||
if (false === $held) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $current ?? new \DateTimeImmutable('today');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class ApplicationFormSubmissionFilterDto extends AbstractFilterDto
|
||||
{
|
||||
protected ?string $name = null;
|
||||
|
||||
/**
|
||||
* A range over the arrival time, not over the follow-up dates: "what came in that week" is
|
||||
* the question a reviewer asks of this list. The datepicker hands over midnight, so the
|
||||
* repository has to stretch dateTo to the end of its day - submittedAt carries a time.
|
||||
*/
|
||||
protected ?\DateTimeImmutable $dateFrom = null;
|
||||
|
||||
protected ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(?string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,11 @@
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ApplicationFormSubmission;
|
||||
use App\Model\ApplicationFormSubmissionFilterDto;
|
||||
use App\Repository\Traits\QueryHelperTrait;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
@@ -16,8 +20,48 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class ApplicationFormSubmissionRepository extends ServiceEntityRepository
|
||||
{
|
||||
use QueryHelperTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ApplicationFormSubmission::class);
|
||||
}
|
||||
|
||||
public function getListQuery(ApplicationFormSubmissionFilterDto $filterDto): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('submission');
|
||||
|
||||
$this->applyFilterCriteria($qb, $filterDto);
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
private function applyFilterCriteria(QueryBuilder $qb, ApplicationFormSubmissionFilterDto $filterDto): void
|
||||
{
|
||||
if (null !== $name = $filterDto->getName()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->orX(
|
||||
$qb->expr()->like('submission.firstName', ':name'),
|
||||
$qb->expr()->like('submission.lastName', ':name')
|
||||
))
|
||||
->setParameter('name', '%'.$this->escapeLikeWildcards($name).'%')
|
||||
;
|
||||
}
|
||||
|
||||
if (null !== $dateFrom = $filterDto->getDateFrom()) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->gte('submission.submittedAt', ':dateFrom'))
|
||||
->setParameter('dateFrom', $dateFrom)
|
||||
;
|
||||
}
|
||||
|
||||
if (null !== $dateTo = $filterDto->getDateTo()) {
|
||||
// The datepicker hands over midnight and submittedAt carries a time, so a plain
|
||||
// lte would drop everything that arrived during the last day of the range.
|
||||
$qb
|
||||
->andWhere($qb->expr()->lte('submission.submittedAt', ':dateTo'))
|
||||
->setParameter('dateTo', $dateTo->setTime(23, 59, 59))
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Common;
|
||||
|
||||
use App\Model\AbstractFilterDto;
|
||||
use App\Model\ApplicationFormSubmissionFilterDto;
|
||||
|
||||
class ApplicationFormSubmissionFilterHandler extends AbstractFilterHandler
|
||||
{
|
||||
protected string $namespace = 'filter:application_form_submission';
|
||||
protected string $modelClass = ApplicationFormSubmissionFilterDto::class;
|
||||
|
||||
public function getFilterSettings(): ApplicationFormSubmissionFilterDto
|
||||
{
|
||||
$filterDto = new $this->modelClass();
|
||||
|
||||
if (null === $data = $this->getSession()->get($this->namespace)) {
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$filterDto->setName($data['name']);
|
||||
}
|
||||
if (isset($data['date_from'])) {
|
||||
$filterDto->setDateFrom($data['date_from']);
|
||||
}
|
||||
if (isset($data['date_to'])) {
|
||||
$filterDto->setDateTo($data['date_to']);
|
||||
}
|
||||
|
||||
return $filterDto;
|
||||
}
|
||||
|
||||
protected function saveFilterSettings(AbstractFilterDto $filterDto): void
|
||||
{
|
||||
/* @var ApplicationFormSubmissionFilterDto $filterDto */
|
||||
$this->getSession()->set($this->namespace, [
|
||||
'name' => $filterDto->getName(),
|
||||
'date_from' => $filterDto->getDateFrom(),
|
||||
'date_to' => $filterDto->getDateTo(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user