feat: all-season assignments
This commit is contained in:
1 parent
675cec1b69
commit
cf876ed224
119 files changed
+6194
-294
No files matched your search
@@ -30,4 +30,22 @@ class TeamerController extends AbstractController
|
||||
|
||||
return $this->json($dates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Season assignments are only ever made for all-season teamers, so their form must not be
|
||||
* able to offer anybody else.
|
||||
*/
|
||||
#[Route('/admin/autocomplete/season-teamer', name: 'app_admin_autocomplete_season_teamer')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function season(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$query = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
$queryString = $query['search'];
|
||||
} catch (\JsonException $e) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
return $this->json($this->teamerRepository->getAutocompletionData($queryString, true));
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class MailingController extends AbstractController
|
||||
));
|
||||
}
|
||||
|
||||
$filterDto = $this->filterHandler->getFilterSettings();
|
||||
$filterDto = $this->filterHandler->setScope($request->query->get('scope'))->getFilterSettings();
|
||||
|
||||
// No redirect after the preview mail, the composed mail has to survive it. The
|
||||
// preview pane is rendered here so the page arrives complete; from then on the
|
||||
@@ -69,7 +69,8 @@ class MailingController extends AbstractController
|
||||
'recipients' => $this->mailingService->resolveRecipients($filterDto),
|
||||
'placeholders' => TeamerMailingService::PLACEHOLDERS,
|
||||
'hasDraft' => $this->draftHandler->hasDraft(),
|
||||
'returnUrl' => $this->getReturnUrl($request, 'app_administrative_teamer_index'),
|
||||
'returnUrl' => $this->getReturnUrl($request, $this->getListRoute($request)),
|
||||
'scope' => $request->query->get('scope'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -94,11 +95,13 @@ class MailingController extends AbstractController
|
||||
|
||||
#[Route('/admin/teamer/mailing/discard', name: 'app_admin_teamer_mailing_discard')]
|
||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||
public function discard(): Response
|
||||
public function discard(Request $request): Response
|
||||
{
|
||||
$this->draftHandler->resetDraft();
|
||||
|
||||
return $this->redirectToRoute('app_admin_teamer_mailing');
|
||||
return $this->redirectToRoute('app_admin_teamer_mailing', [
|
||||
'scope' => $request->query->get('scope'),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/teamer/mailing/confirm', name: 'app_admin_teamer_mailing_confirm', methods: ['POST'])]
|
||||
@@ -106,7 +109,7 @@ class MailingController extends AbstractController
|
||||
public function confirm(Request $request): Response
|
||||
{
|
||||
$form = $this->createMailingForm($request);
|
||||
$filterDto = $this->filterHandler->getFilterSettings();
|
||||
$filterDto = $this->filterHandler->setScope($request->query->get('scope'))->getFilterSettings();
|
||||
|
||||
// the composed mail is carried on in hidden fields so no half written mailing has
|
||||
// to be parked in the session between the confirmation and the send
|
||||
@@ -115,6 +118,7 @@ class MailingController extends AbstractController
|
||||
'mailingDto' => $form->getData(),
|
||||
'valid' => $form->isSubmitted() && $form->isValid(),
|
||||
'recipients' => $this->mailingService->resolveRecipients($filterDto),
|
||||
'scope' => $request->query->get('scope'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -127,10 +131,14 @@ class MailingController extends AbstractController
|
||||
if (false === $form->isSubmitted() || false === $form->isValid()) {
|
||||
$this->addFlash('error', 'Die Mail konnte nicht gesendet werden, Betreff oder Nachricht fehlen');
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_admin_teamer_mailing'));
|
||||
return new HxRedirectResponse($this->generateUrl('app_admin_teamer_mailing', [
|
||||
'scope' => $request->query->get('scope'),
|
||||
]));
|
||||
}
|
||||
|
||||
$recipients = $this->mailingService->resolveRecipients($this->filterHandler->getFilterSettings());
|
||||
$recipients = $this->mailingService->resolveRecipients(
|
||||
$this->filterHandler->setScope($request->query->get('scope'))->getFilterSettings()
|
||||
);
|
||||
$mailingDto = $form->getData();
|
||||
|
||||
// the recipients travel with the message, so the mailing reaches exactly the
|
||||
@@ -151,7 +159,18 @@ class MailingController extends AbstractController
|
||||
$recipients->getEligibleCount()
|
||||
));
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_teamer_index'));
|
||||
return new HxRedirectResponse($this->generateUrl($this->getListRoute($request)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The list this mailing was started from, so a send returns to it and an unscoped
|
||||
* mailing never falls back to the wrong one.
|
||||
*/
|
||||
private function getListRoute(Request $request): string
|
||||
{
|
||||
return TeamerFilterHandler::SCOPE_ALL_SEASON === $request->query->get('scope')
|
||||
? 'app_administrative_season_teamer_index'
|
||||
: 'app_administrative_teamer_index';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,8 @@ class SkillsController extends AbstractController
|
||||
{
|
||||
$trainings = $this->trainingRepository->getList();
|
||||
|
||||
$allSeasonStatus = $teamer->isAllSeason();
|
||||
|
||||
$form = $this->createStatusForm($teamer);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -42,6 +44,15 @@ class SkillsController extends AbstractController
|
||||
'teamer' => $teamer->getFullName(),
|
||||
]);
|
||||
|
||||
if ($teamer->isAllSeason() !== $allSeasonStatus) {
|
||||
$this->addFlash('info', 'Der Saisonteamer:innern-Status wurde '.($teamer->isAllSeason() ? 'hinzugefügt' : 'entfernt'));
|
||||
|
||||
$logMessage = $teamer->isAllSeason() ? 'Assign all-season status' : 'Remove all-season status';
|
||||
$this->logger->info($logMessage, [
|
||||
'teamer' => $teamer->getFullName(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_teamer_skills', [
|
||||
'uuid' => $teamer->getUuid(),
|
||||
'r' => $request->query->get('r'),
|
||||
@@ -62,10 +73,14 @@ class SkillsController extends AbstractController
|
||||
->add('status', ChoiceType::class, [
|
||||
'label' => 'Status',
|
||||
'choices' => [
|
||||
'Neuteamer' => Teamer::STATUS_NEW,
|
||||
'Bestandsteamer' => Teamer::STATUS_EXISTING,
|
||||
'Neuteamer:in' => Teamer::STATUS_NEW,
|
||||
'Bestandsteamer:in' => Teamer::STATUS_EXISTING,
|
||||
],
|
||||
])
|
||||
->add('allSeason', CheckboxType::class, [
|
||||
'label' => 'Saisonteamer:in',
|
||||
'required' => false,
|
||||
])
|
||||
->add('allowOverlappingApplications', CheckboxType::class, [
|
||||
'label' => 'Überlappende Bewerbungen erlauben',
|
||||
'required' => false,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Config\SeasonCalendar;
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Form\SeasonAssignmentType;
|
||||
use App\Service\Season\SeasonAssignmentLabeler;
|
||||
use App\Service\Season\SeasonTaskSynchronizer;
|
||||
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;
|
||||
|
||||
class CreateController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly SeasonTaskSynchronizer $taskSynchronizer,
|
||||
private readonly SeasonAssignmentLabeler $labeler,
|
||||
private readonly SeasonCalendar $seasonCalendar,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/season-assignment/create', name: 'app_administrative_season_assignment_create')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$seasonAssignment = new SeasonAssignment();
|
||||
|
||||
// Almost every assignment laid out runs the full season, so the form opens on the
|
||||
// season that is running today - or, off-season, the one that starts next, the same
|
||||
// window the teamer's season page already falls forward to.
|
||||
[$windowStart, $windowEnd] = $this->seasonCalendar->windowFor($this->seasonCalendar->currentOrUpcomingSeasonYear());
|
||||
$seasonAssignment
|
||||
->setDateFrom($windowStart->toDateTimeImmutable())
|
||||
->setDateTo($windowEnd->toDateTimeImmutable())
|
||||
;
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_season_assignment_index');
|
||||
|
||||
$form = $this->createForm(SeasonAssignmentType::class, $seasonAssignment);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->persist($seasonAssignment);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// the checklist is what the office works with from here on, so it exists from the
|
||||
// moment the assignment does
|
||||
$this->taskSynchronizer->sync($seasonAssignment);
|
||||
|
||||
$this->addFlash('success', 'Der Saisoneinsatz wurde angelegt');
|
||||
|
||||
$this->logger->info('Create season assignment', [
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'teamer' => $seasonAssignment->getTeamer()?->getFullName(),
|
||||
'season_assignment' => $this->labeler->label($seasonAssignment),
|
||||
'job_profile' => $seasonAssignment->getJobProfile()->getName(),
|
||||
]);
|
||||
|
||||
return $this->redirect($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('administrative/season_assignment/create.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'returnUrl' => $returnUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Service\Season\SeasonAssignmentLabeler;
|
||||
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;
|
||||
|
||||
class DeleteController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly SeasonAssignmentLabeler $labeler,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/season-assignment/delete/{uuid}', name: 'app_administrative_season_assignment_delete')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonAssignment $seasonAssignment, Request $request): Response
|
||||
{
|
||||
if (true === $request->isMethod('POST')) {
|
||||
$seasonAssignment->setDeleted();
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Delete season assignment', [
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'teamer' => $seasonAssignment->getTeamer()?->getFullName(),
|
||||
'season_assignment' => $this->labeler->label($seasonAssignment),
|
||||
'job_profile' => $seasonAssignment->getJobProfile()->getName(),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Der Saisoneinsatz wurde gelöscht');
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_season_assignment_index'));
|
||||
}
|
||||
|
||||
return $this->render('administrative/season_assignment/modal_delete.html.twig', [
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Service\Season\SeasonChecklistBuilder;
|
||||
use App\Service\Season\SeasonTaskSynchronizer;
|
||||
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;
|
||||
|
||||
public function __construct(
|
||||
private readonly SeasonTaskSynchronizer $taskSynchronizer,
|
||||
private readonly SeasonChecklistBuilder $checklistBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/season-assignment/detail/{uuid}', name: 'app_administrative_season_assignment_detail')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonAssignment $seasonAssignment, Request $request): Response
|
||||
{
|
||||
// one of the two write paths the synchronizer runs on, so an entry added to the enum
|
||||
// later shows up on assignments that already exist
|
||||
$this->taskSynchronizer->sync($seasonAssignment);
|
||||
|
||||
return $this->render('administrative/season_assignment/detail.html.twig', [
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
'checklist' => $this->checklistBuilder->build($seasonAssignment),
|
||||
'returnUrl' => $this->getReturnUrl($request, 'app_administrative_season_assignment_index'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Entity\Upload;
|
||||
use App\Entity\User;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Model\UploadDto;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Flagception\Manager\FeatureManagerInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The documents the office issues for one season assignment.
|
||||
*
|
||||
* Deliberately not Upload::TYPE_DOCUMENT: that type is the staff-wide library, which UploadVoter
|
||||
* opens to every teamer - an individually issued contract in it would be readable by everyone.
|
||||
* These carry TYPE_SEASON_DOCUMENT_OUT instead, and the voter lets exactly the teamer of this
|
||||
* assignment download them.
|
||||
*
|
||||
* There is no relation between a file and a checklist entry on purpose: the office looks at the
|
||||
* documents, decides which entry a file satisfies and ticks it off by hand.
|
||||
*/
|
||||
class DocumentController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UploadHandler $uploadHandler,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly FeatureManagerInterface $featureManager,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/season-assignment/{uuid}/document',
|
||||
name: 'app_administrative_season_assignment_document_upload'
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function upload(SeasonAssignment $seasonAssignment, Request $request): Response
|
||||
{
|
||||
// Dummy form without fields, as elsewhere: the files travel in the upload session, not in
|
||||
// the form itself.
|
||||
$form = $this->createFormBuilder()->getForm();
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$count = $this->processUpload($seasonAssignment);
|
||||
|
||||
$this->addFlash(
|
||||
'success',
|
||||
1 === $count ? 'Die Unterlage wurde hochgeladen' : 'Die Unterlagen wurden hochgeladen'
|
||||
);
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_season_assignment_detail', [
|
||||
'uuid' => $seasonAssignment->getUuid(),
|
||||
'r' => $request->query->get('r'),
|
||||
]));
|
||||
}
|
||||
|
||||
return $this->render('administrative/season_assignment/modal_document_upload.html.twig', [
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/season-assignment/document/{uuid}',
|
||||
name: 'app_administrative_season_assignment_document_delete'
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function delete(Upload $document, Request $request): Response
|
||||
{
|
||||
if (null === $seasonAssignment = $document->getSeasonAssignment()) {
|
||||
throw $this->createNotFoundException('Not a season assignment document');
|
||||
}
|
||||
|
||||
if (true === $request->isMethod('POST')) {
|
||||
// Doctrine's DeleteUploadListener removes the file from disk on preRemove.
|
||||
$this->entityManager->remove($document);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Die Unterlage wurde gelöscht');
|
||||
|
||||
$this->logger->info('Delete season assignment document', [
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'teamer' => $seasonAssignment->getTeamer()?->getFullName(),
|
||||
'document_filename' => $document->getOriginalFilename(),
|
||||
'document_type' => $document->getType(),
|
||||
]);
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_season_assignment_detail', [
|
||||
'uuid' => $seasonAssignment->getUuid(),
|
||||
'r' => $request->query->get('r'),
|
||||
]));
|
||||
}
|
||||
|
||||
return $this->render('administrative/season_assignment/modal_document_delete.html.twig', [
|
||||
'document' => $document,
|
||||
]);
|
||||
}
|
||||
|
||||
private function processUpload(SeasonAssignment $seasonAssignment): int
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$uploadSession = $this->uploadHandler->getUploadSession();
|
||||
|
||||
// Only this dropzone's files: the page may hold a second one, so the session must not be
|
||||
// destroyed wholesale.
|
||||
$uploadDtos = $uploadSession->getUploadsByType(Upload::TYPE_SEASON_DOCUMENT_OUT);
|
||||
|
||||
$uploads = [];
|
||||
|
||||
/** @var UploadDto $uploadDto */
|
||||
foreach ($uploadDtos as $uploadDto) {
|
||||
$upload = Upload::fromUploadDto($uploadDto, $user, Upload::TYPE_SEASON_DOCUMENT_OUT);
|
||||
|
||||
// No status: these are not checked or approved, they are handed out. UploadVoter
|
||||
// grants CHECK only for "new" and "pending", so a null status keeps them out of the
|
||||
// checking process by construction.
|
||||
$upload->setStatus(null);
|
||||
|
||||
$seasonAssignment->addDocument($upload);
|
||||
$this->entityManager->persist($upload);
|
||||
|
||||
$uploads[] = $upload;
|
||||
}
|
||||
|
||||
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_SEASON_DOCUMENT_OUT, $uploadSession);
|
||||
$this->uploadHandler->removeUploadSessionUploadsByType(Upload::TYPE_SEASON_DOCUMENT_OUT);
|
||||
|
||||
if (true === $this->featureManager->isActive('sanitize_uploads')) {
|
||||
foreach ($uploads as $upload) {
|
||||
$this->uploadHandler->ensurePdf($upload);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Upload season assignment documents', [
|
||||
'user' => $user->getUserIdentifier(),
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'count' => count($uploads),
|
||||
]);
|
||||
|
||||
return count($uploads);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Entity\SeasonAssignment;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Toggles the office's note that it considers this teamer done.
|
||||
*
|
||||
* Informative only: it silences nothing. The weekly teamer digest and the office deadline alerts
|
||||
* both keep running while a return-required checklist entry is open. Administrative users only -
|
||||
* there is no teamer-side write path.
|
||||
*/
|
||||
class DoneTeamerController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/season-assignment/{uuid}/done-teamer',
|
||||
name: 'app_administrative_season_assignment_done_teamer',
|
||||
methods: ['POST']
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonAssignment $seasonAssignment): Response
|
||||
{
|
||||
$seasonAssignment->setDoneTeamer(false === $seasonAssignment->isDoneTeamer());
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Toggle season assignment done teamer', [
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'teamer' => $seasonAssignment->getTeamer()?->getFullName(),
|
||||
'value' => $seasonAssignment->isDoneTeamer(),
|
||||
]);
|
||||
|
||||
return $this->render('administrative/season_assignment/_done_teamer.html.twig', [
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Form\SeasonAssignmentType;
|
||||
use App\Service\Season\SeasonAssignmentLabeler;
|
||||
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;
|
||||
|
||||
class EditController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly SeasonAssignmentLabeler $labeler,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/season-assignment/edit/{uuid}', name: 'app_administrative_season_assignment_edit')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonAssignment $seasonAssignment, Request $request): Response
|
||||
{
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_season_assignment_index');
|
||||
|
||||
$form = $this->createForm(SeasonAssignmentType::class, $seasonAssignment);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Der Saisoneinsatz wurde gespeichert');
|
||||
|
||||
$this->logger->info('Edit season assignment', [
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'teamer' => $seasonAssignment->getTeamer()?->getFullName(),
|
||||
'season_assignment' => $this->labeler->label($seasonAssignment),
|
||||
'job_profile' => $seasonAssignment->getJobProfile()->getName(),
|
||||
]);
|
||||
|
||||
return $this->redirect($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('administrative/season_assignment/edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
'returnUrl' => $returnUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Repository\SeasonAssignmentRepository;
|
||||
use App\Service\Common\SeasonAssignmentFilterHandler;
|
||||
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;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SeasonAssignmentFilterHandler $filterHandler,
|
||||
private readonly SeasonAssignmentRepository $seasonAssignmentRepository,
|
||||
private readonly PaginatorInterface $paginator,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/season-assignment', name: 'app_administrative_season_assignment_index')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$filterDto = $this->filterHandler->getFilterSettings();
|
||||
|
||||
$query = $this
|
||||
->seasonAssignmentRepository
|
||||
->getListQuery($filterDto)
|
||||
;
|
||||
|
||||
$pagination = $this->paginator->paginate(
|
||||
$query,
|
||||
$request->query->getInt('page', 1),
|
||||
$request->query->getInt('limit', 50)
|
||||
);
|
||||
|
||||
return $this->render('administrative/season_assignment/index.html.twig', [
|
||||
'pagination' => $pagination,
|
||||
'filterDto' => $filterDto,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Entity\SeasonAssignmentTask;
|
||||
use App\Form\SeasonTaskDeadlineType;
|
||||
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;
|
||||
|
||||
class TaskDeadlineController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/season-assignment/task/{uuid}/deadline',
|
||||
name: 'app_administrative_season_assignment_task_deadline'
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonAssignmentTask $task, Request $request): Response
|
||||
{
|
||||
$seasonAssignment = $task->getSeasonAssignment();
|
||||
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_season_assignment_detail', [
|
||||
'uuid' => $seasonAssignment?->getUuid(),
|
||||
]);
|
||||
|
||||
$form = $this->createForm(SeasonTaskDeadlineType::class, $task, [
|
||||
'action' => $this->generateUrl('app_administrative_season_assignment_task_deadline', [
|
||||
'uuid' => $task->getUuid(),
|
||||
'r' => $returnUrl,
|
||||
]),
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Change season assignment task deadlines', [
|
||||
'season_assignment_id' => $seasonAssignment?->getId(),
|
||||
'teamer' => $seasonAssignment?->getTeamer()?->getFullName(),
|
||||
'task' => $task->getTaskKey(),
|
||||
'deadline_override_1' => $task->getDeadlineOverride1()?->format('Y-m-d'),
|
||||
'deadline_override_2' => $task->getDeadlineOverride2()?->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Die Fristen wurden gespeichert');
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('administrative/season_assignment/modal_task_deadline.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'task' => $task,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\SeasonAssignment;
|
||||
|
||||
use App\Entity\SeasonAssignmentTask;
|
||||
use App\Service\Season\SeasonChecklistBuilder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Ticks one checklist entry, re-rendering that single row.
|
||||
*
|
||||
* Deliberately one request per row rather than one form for the whole checklist: the office
|
||||
* works through the list one line at a time while reading a document, and a whole-form submit
|
||||
* would let two people working the same assignment overwrite each other's ticks.
|
||||
*/
|
||||
class TaskToggleController extends AbstractController
|
||||
{
|
||||
private const FIELD_DONE = 'done';
|
||||
private const FIELD_UPLOADED = 'uploaded';
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly SeasonChecklistBuilder $checklistBuilder,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/season-assignment/task/{uuid}/{field}',
|
||||
name: 'app_administrative_season_assignment_task_toggle',
|
||||
// pinned in the route as well as checked below, so this can never shadow the sibling
|
||||
// .../task/{uuid}/deadline route whichever order the two are loaded in
|
||||
requirements: ['field' => 'done|uploaded'],
|
||||
methods: ['POST']
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonAssignmentTask $task, string $field): Response
|
||||
{
|
||||
// matched against the two names rather than turned into a property path: the field comes
|
||||
// out of the URL
|
||||
if (false === in_array($field, [self::FIELD_DONE, self::FIELD_UPLOADED], true)) {
|
||||
throw $this->createNotFoundException(sprintf('Unknown checklist field "%s".', $field));
|
||||
}
|
||||
|
||||
if (self::FIELD_DONE === $field) {
|
||||
$done = false === $task->isDone();
|
||||
|
||||
$task
|
||||
->setDone($done)
|
||||
// explicit, because Blameable's updatedBy is overwritten by any later edit and
|
||||
// the office wants to see who closed the entry
|
||||
->setDoneAt(true === $done ? new \DateTimeImmutable() : null)
|
||||
->setDoneBy(true === $done ? $this->getUser()?->getUserIdentifier() : null)
|
||||
;
|
||||
} else {
|
||||
$task->setUploaded(false === $task->isUploaded());
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Toggle season assignment task', [
|
||||
'season_assignment_id' => $task->getSeasonAssignment()?->getId(),
|
||||
'teamer' => $task->getSeasonAssignment()?->getTeamer()?->getFullName(),
|
||||
'task' => $task->getTaskKey(),
|
||||
'field' => $field,
|
||||
'value' => self::FIELD_DONE === $field ? $task->isDone() : $task->isUploaded(),
|
||||
]);
|
||||
|
||||
return $this->render('administrative/season_assignment/_task_row.html.twig', [
|
||||
'row' => $this->checklistBuilder->buildRow($task),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\System\SeasonTaskDocument;
|
||||
|
||||
use App\Entity\SeasonTaskDocument;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Removes the mapping, not the document: the library file itself stays where it is and is
|
||||
* managed under Dokumente.
|
||||
*/
|
||||
class DeleteController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/system/season-task-document/delete/{uuid}',
|
||||
name: 'app_administrative_system_season_task_document_delete'
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(SeasonTaskDocument $seasonTaskDocument, Request $request): Response
|
||||
{
|
||||
if (true === $request->isMethod('POST')) {
|
||||
$taskKey = $seasonTaskDocument->getTaskKey();
|
||||
|
||||
$this->entityManager->remove($seasonTaskDocument);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Remove season task document', ['task' => $taskKey]);
|
||||
|
||||
$this->addFlash('success', 'Die Zuordnung wurde entfernt');
|
||||
|
||||
return new HxRedirectResponse(
|
||||
$this->generateUrl('app_administrative_system_season_task_document_index')
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('administrative/system/season_task_document/modal_delete.html.twig', [
|
||||
'seasonTaskDocument' => $seasonTaskDocument,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\System\SeasonTaskDocument;
|
||||
|
||||
use App\Config\SeasonTask;
|
||||
use App\Config\SeasonTaskProvision;
|
||||
use App\Entity\SeasonTaskDocument;
|
||||
use App\Form\SeasonTaskDocumentType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Repository\SeasonTaskDocumentRepository;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Sets the library document a checklist entry hands out. Addressed by the entry rather than by a
|
||||
* record: the mapping may not exist yet, and there is at most one per entry.
|
||||
*/
|
||||
class EditController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SeasonTaskDocumentRepository $seasonTaskDocumentRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/administrative/system/season-task-document/edit/{taskKey}',
|
||||
name: 'app_administrative_system_season_task_document_edit'
|
||||
)]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(string $taskKey, Request $request): Response
|
||||
{
|
||||
$task = SeasonTask::tryFrom($taskKey);
|
||||
|
||||
if (null === $task || SeasonTaskProvision::STANDARD !== $task->provides()) {
|
||||
throw $this->createNotFoundException(sprintf('No standard document is handed out for "%s".', $taskKey));
|
||||
}
|
||||
|
||||
$seasonTaskDocument = $this->seasonTaskDocumentRepository->findForTask($task)
|
||||
?? (new SeasonTaskDocument())->setTaskKey($task->value);
|
||||
|
||||
$form = $this->createForm(SeasonTaskDocumentType::class, $seasonTaskDocument);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
if (null === $seasonTaskDocument->getId()) {
|
||||
$this->entityManager->persist($seasonTaskDocument);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Change season task document', [
|
||||
'task' => $task->value,
|
||||
'document_id' => $seasonTaskDocument->getDocument()?->getId(),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Das Dokument wurde hinterlegt');
|
||||
|
||||
return new HxRedirectResponse(
|
||||
$this->generateUrl('app_administrative_system_season_task_document_index')
|
||||
);
|
||||
}
|
||||
|
||||
return $this->render('administrative/system/season_task_document/modal_edit.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
'task' => $task,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\System\SeasonTaskDocument;
|
||||
|
||||
use App\Config\SeasonTask;
|
||||
use App\Config\SeasonTaskProvision;
|
||||
use App\Repository\SeasonTaskDocumentRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Which document from the global library a checklist entry hands out.
|
||||
*
|
||||
* The rows are the enum's entries, not database records: only the entries that hand out a
|
||||
* standard document can carry a mapping at all, and one without a mapping is a valid - if
|
||||
* incomplete - state that the teamer's page has to show as "kein Dokument hinterlegt".
|
||||
*/
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly SeasonTaskDocumentRepository $seasonTaskDocumentRepository)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/administrative/system/season-task-document', name: 'app_administrative_system_season_task_document_index')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(): Response
|
||||
{
|
||||
$mapped = $this->seasonTaskDocumentRepository->findIndexedByTaskKey();
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach (SeasonTask::cases() as $case) {
|
||||
if (SeasonTaskProvision::STANDARD !== $case->provides()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'task' => $case,
|
||||
'document' => $mapped[$case->value] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->render('administrative/system/season_task_document/index.html.twig', [
|
||||
'rows' => $rows,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly TeamerFilterHandler $filterHandler,
|
||||
private readonly TeamerFilterHandler $filterHandler,
|
||||
private readonly TeamerRepository $teamerRepository,
|
||||
private readonly PaginatorInterface $paginator,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
@@ -28,7 +28,38 @@ class IndexController extends AbstractController
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$filterDto = $this->filterHandler->getFilterSettings();
|
||||
return $this->renderList($request, 'administrative/teamer/index.html.twig', null);
|
||||
}
|
||||
|
||||
#[Route('/administrative/season-teamer', name: 'app_administrative_season_teamer_index')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function season(Request $request): Response
|
||||
{
|
||||
return $this->renderList($request, 'administrative/teamer/season_index.html.twig', TeamerFilterHandler::SCOPE_ALL_SEASON);
|
||||
}
|
||||
|
||||
#[Route('/administrative/teamer/viewed/{uuid}', name: 'app_administrative_teamer_viewed', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function viewed(Teamer $teamer): Response
|
||||
{
|
||||
$teamer->setViewed(true);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_teamer_index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Both teamer lists differ only in the scope they run under, so they share one body.
|
||||
* The scope is set explicitly on either path to keep the two independent of the order
|
||||
* in which they are called within a request.
|
||||
*/
|
||||
private function renderList(Request $request, string $template, ?string $scope): Response
|
||||
{
|
||||
$filterDto = $this
|
||||
->filterHandler
|
||||
->setScope($scope)
|
||||
->getFilterSettings()
|
||||
;
|
||||
|
||||
$query = $this
|
||||
->teamerRepository
|
||||
@@ -41,19 +72,9 @@ class IndexController extends AbstractController
|
||||
$request->query->getInt('limit', 50)
|
||||
);
|
||||
|
||||
return $this->render('administrative/teamer/index.html.twig', [
|
||||
return $this->render($template, [
|
||||
'pagination' => $pagination,
|
||||
'filterDto' => $filterDto,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/administrative/teamer/viewed/{uuid}', name: 'app_administrative_teamer_viewed', methods: ['POST'])]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function viewed(Teamer $teamer): Response
|
||||
{
|
||||
$teamer->setViewed(true);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_teamer_index'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Common;
|
||||
|
||||
use App\Controller\Traits\ReturnUrlTrait;
|
||||
use App\Form\SeasonAssignmentFilterType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Service\Common\SeasonAssignmentFilterHandler;
|
||||
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 SeasonAssignmentFilterController extends AbstractController
|
||||
{
|
||||
use ReturnUrlTrait;
|
||||
|
||||
public function __construct(private readonly SeasonAssignmentFilterHandler $filterHandler)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/common/season-assignment/filter', name: 'app_common_season_assignment_filter')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$formData = $this->filterHandler->getFilterSettings();
|
||||
$form = $this->createForm(SeasonAssignmentFilterType::class, $formData);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->filterHandler->handleRequest($form);
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_season_assignment_index');
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
|
||||
return $this->render('common/modal_season_assignment_filter.html.twig', [
|
||||
'filterForm' => $form->createView(),
|
||||
'filterDto' => $form->getData(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/common/season-assignment/filter/reset', name: 'app_common_season_assignment_filter_reset')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function reset(Request $request): Response
|
||||
{
|
||||
$this->filterHandler->resetFilterSettings();
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_season_assignment_index');
|
||||
|
||||
return $this->redirect($returnUrl);
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,15 @@ class TeamerFilterController extends AbstractController
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->applyScope($request);
|
||||
|
||||
$formData = $this->filterHandler->getFilterSettings();
|
||||
$form = $this->createForm(TeamerFilterType::class, $formData);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->filterHandler->handleRequest($form);
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_teamer_index');
|
||||
$returnUrl = $this->getReturnUrl($request, $this->getDefaultRoute($request));
|
||||
|
||||
return new HxRedirectResponse($returnUrl);
|
||||
}
|
||||
@@ -45,9 +47,26 @@ class TeamerFilterController extends AbstractController
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function reset(Request $request): Response
|
||||
{
|
||||
$this->applyScope($request);
|
||||
|
||||
$this->filterHandler->resetFilterSettings();
|
||||
$returnUrl = $this->getReturnUrl($request, 'app_administrative_teamer_index');
|
||||
$returnUrl = $this->getReturnUrl($request, $this->getDefaultRoute($request));
|
||||
|
||||
return $this->redirect($returnUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* The modal serves both teamer lists; the scope says whose filter is being edited.
|
||||
*/
|
||||
private function applyScope(Request $request): void
|
||||
{
|
||||
$this->filterHandler->setScope($request->query->get('scope'));
|
||||
}
|
||||
|
||||
private function getDefaultRoute(Request $request): string
|
||||
{
|
||||
return TeamerFilterHandler::SCOPE_ALL_SEASON === $request->query->get('scope')
|
||||
? 'app_administrative_season_teamer_index'
|
||||
: 'app_administrative_teamer_index';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Teamer\Season;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Service\Season\SeasonAssignmentResolver;
|
||||
use App\Service\Season\SeasonChecklistBuilder;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* What an all-season teamer has to download, sign and return.
|
||||
*
|
||||
* Only the entries expecting something back are listed, whether or not the office issues a
|
||||
* document for them first; the office-internal checks never appear here. Frist 2 does not either:
|
||||
* it is an internal escalation.
|
||||
*/
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SeasonAssignmentResolver $seasonAssignmentResolver,
|
||||
private readonly SeasonChecklistBuilder $checklistBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/teamer/season', name: 'app_teamer_season_index')]
|
||||
#[IsGranted('ROLE_TEAMER')]
|
||||
public function index(): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (null === $teamer = $user->getTeamer()) {
|
||||
throw $this->createNotFoundException('No teamer for this user');
|
||||
}
|
||||
|
||||
if (null === $seasonAssignment = $this->seasonAssignmentResolver->resolveFor($teamer)) {
|
||||
throw $this->createNotFoundException('No season assignment for this teamer');
|
||||
}
|
||||
|
||||
$rows = array_filter(
|
||||
$this->checklistBuilder->build($seasonAssignment),
|
||||
static fn (array $row): bool => true === $row['definition']->requiresReturn()
|
||||
);
|
||||
|
||||
return $this->render('teamer/season/index.html.twig', [
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
'rows' => $rows,
|
||||
'documents' => $seasonAssignment->getDocuments(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Teamer\Season;
|
||||
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Entity\Upload;
|
||||
use App\Entity\User;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use App\Model\UploadDto;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Flagception\Manager\FeatureManagerInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The signed documents a teamer returns for their season assignment.
|
||||
*
|
||||
* They accumulate, like Belege: there is no relation between a file and a checklist entry, and
|
||||
* nothing here closes one - only an administrative tick on "erledigt" does that.
|
||||
*/
|
||||
class UploadController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UploadHandler $uploadHandler,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly FeatureManagerInterface $featureManager,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/teamer/season/upload/{uuid}', name: 'app_teamer_season_upload')]
|
||||
#[IsGranted('ROLE_TEAMER')]
|
||||
public function index(SeasonAssignment $seasonAssignment, Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
// the assignment is addressed by uuid, so this is what keeps one teamer out of another's
|
||||
if ($user->getTeamer() !== $seasonAssignment->getTeamer()) {
|
||||
throw $this->createNotFoundException('Not this teamer\'s season assignment');
|
||||
}
|
||||
|
||||
// Dummy form without fields, as elsewhere: the files travel in the upload session.
|
||||
$form = $this->createFormBuilder()->getForm();
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$count = $this->processUpload($seasonAssignment, $user);
|
||||
|
||||
$this->addFlash(
|
||||
'success',
|
||||
1 === $count ? 'Das Dokument wurde hochgeladen' : 'Die Dokumente wurden hochgeladen'
|
||||
);
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_teamer_season_index'));
|
||||
}
|
||||
|
||||
return $this->render('teamer/season/modal_upload.html.twig', [
|
||||
'seasonAssignment' => $seasonAssignment,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function processUpload(SeasonAssignment $seasonAssignment, User $user): int
|
||||
{
|
||||
$uploadSession = $this->uploadHandler->getUploadSession();
|
||||
|
||||
// Only this dropzone's files: the session is shared, so it must not be destroyed whole.
|
||||
$uploadDtos = $uploadSession->getUploadsByType(Upload::TYPE_SEASON_DOCUMENT_IN);
|
||||
|
||||
$uploads = [];
|
||||
|
||||
/** @var UploadDto $uploadDto */
|
||||
foreach ($uploadDtos as $uploadDto) {
|
||||
$upload = Upload::fromUploadDto($uploadDto, $user, Upload::TYPE_SEASON_DOCUMENT_IN);
|
||||
|
||||
// No status: a returned document is not run through the checking process, the office
|
||||
// ticks the checklist entry off by hand.
|
||||
$upload->setStatus(null);
|
||||
|
||||
$seasonAssignment->addDocument($upload);
|
||||
$this->entityManager->persist($upload);
|
||||
|
||||
$uploads[] = $upload;
|
||||
}
|
||||
|
||||
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_SEASON_DOCUMENT_IN, $uploadSession);
|
||||
$this->uploadHandler->removeUploadSessionUploadsByType(Upload::TYPE_SEASON_DOCUMENT_IN);
|
||||
|
||||
if (true === $this->featureManager->isActive('sanitize_uploads')) {
|
||||
foreach ($uploads as $upload) {
|
||||
$this->uploadHandler->ensurePdf($upload);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Upload season documents', [
|
||||
'user' => $user->getUserIdentifier(),
|
||||
'season_assignment_id' => $seasonAssignment->getId(),
|
||||
'count' => count($uploads),
|
||||
]);
|
||||
|
||||
return count($uploads);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user