feat: refactor teamer-document upload for admins

This commit is contained in:
Björn Fromme
2025-01-08 18:00:25 +01:00
parent dad6965494
commit 3d20d5cf94
5 changed files with 32 additions and 58 deletions
@@ -10,12 +10,10 @@ use App\Service\Upload\UploadHandler;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class DocumentUploadController extends AbstractController class DocumentUploadController extends AbstractController
@@ -23,8 +21,6 @@ class DocumentUploadController extends AbstractController
public function __construct( public function __construct(
private readonly UploadHandler $uploadHandler, private readonly UploadHandler $uploadHandler,
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
#[Target('disposition')]
private readonly WorkflowInterface $workflow,
private readonly EventDispatcherInterface $eventDispatcher, private readonly EventDispatcherInterface $eventDispatcher,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
) { ) {
@@ -34,48 +30,35 @@ class DocumentUploadController extends AbstractController
path: '/administrative/disposition/document-upload/{uuid}', path: '/administrative/disposition/document-upload/{uuid}',
name: 'app_administrative_disposition_document_upload' name: 'app_administrative_disposition_document_upload'
)] )]
#[IsGranted('ROLE_ADMINISTRATIVE')] #[IsGranted('CONTRACT_SUPPLEMENTARY', subject: 'disposition')]
public function index(Disposition $disposition, Request $request): Response public function index(Disposition $disposition, Request $request): Response
{ {
if (true === $this->workflow->can($disposition, 'upload_contract')) {
$uploadType = Upload::TYPE_CONTRACT;
$uploadTypeLabel = 'Honorarvertrag';
$uploaderRoute = '_uploader_upload_contract';
} elseif (true === $this->workflow->can($disposition, 'upload_invoice')) {
$uploadType = Upload::TYPE_INVOICE;
$uploadTypeLabel = 'Honoranote';
$uploaderRoute = '_uploader_upload_invoice';
} else {
$this->addFlash('error', 'Kein Upload möglich beim aktuellen Status der Einteilung');
return $this->getRedirectResponse($disposition);
}
$teamer = $disposition->getTeamer(); $teamer = $disposition->getTeamer();
$modalTitle = sprintf('%s für %s hochladen', $uploadTypeLabel, $teamer); $modalTitle = sprintf('Honorarvertrag für %s hochladen', $teamer);
$form = $this->createFormBuilder()->getForm(); $form = $this->createFormBuilder()->getForm();
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$this->processUpload($disposition, $uploadType); $this->processUpload($disposition);
$this->addFlash('success', sprintf('%s wurde hochgeladen', $uploadTypeLabel)); $this->addFlash('success', 'Der Honorarvertrag wurde hochgeladen');
return $this->getRedirectResponse($disposition); return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(),
]));
} }
return $this->render('administrative/disposition/modal_document_upload.html.twig', [ return $this->render('administrative/disposition/modal_document_upload.html.twig', [
'disposition' => $disposition, 'disposition' => $disposition,
'modalTitle' => $modalTitle, 'modalTitle' => $modalTitle,
'uploaderRoute' => $uploaderRoute,
'form' => $form->createView(), 'form' => $form->createView(),
]); ]);
} }
private function processUpload(Disposition $disposition, string $uploadType): void private function processUpload(Disposition $disposition): void
{ {
if (null !== $existingUpload = $disposition->getDocumentByType($uploadType)) { if (null !== $existingUpload = $disposition->getDocumentByType(Upload::TYPE_CONTRACT)) {
$this->entityManager->remove($existingUpload); $this->entityManager->remove($existingUpload);
} }
@@ -84,17 +67,14 @@ class DocumentUploadController extends AbstractController
$uploadSession = $this->uploadHandler->getUploadSession(); $uploadSession = $this->uploadHandler->getUploadSession();
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, $uploadType); $upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_CONTRACT);
$upload->setStatus(Upload::STATUS_NEW); $upload->setStatus(Upload::STATUS_CHECKED);
$disposition->addDocument($upload); $disposition->addDocument($upload);
$transitionName = sprintf('upload_%s', $uploadType);
$this->workflow->apply($disposition, $transitionName);
$this->entityManager->flush(); $this->entityManager->flush();
$this->uploadHandler->moveUploadSessionFilesFromOrphanage($uploadType, $uploadSession); $this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_CONTRACT, $uploadSession);
$this->uploadHandler->destroyUploadSession(); $this->uploadHandler->destroyUploadSession();
$this->eventDispatcher->dispatch(new DocumentUploadedEvent($upload), DocumentUploadedEvent::NAME); $this->eventDispatcher->dispatch(new DocumentUploadedEvent($upload), DocumentUploadedEvent::NAME);
@@ -104,14 +84,7 @@ class DocumentUploadController extends AbstractController
'teamer_name' => $teamer->getFullName(), 'teamer_name' => $teamer->getFullName(),
'disposition_id' => $disposition->getId(), 'disposition_id' => $disposition->getId(),
'document_id' => $upload->getId(), 'document_id' => $upload->getId(),
'upload_type' => $uploadType, 'upload_type' => Upload::TYPE_CONTRACT,
]); ]);
} }
private function getRedirectResponse(Disposition $disposition): HxRedirectResponse
{
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(),
]));
}
} }
@@ -4,14 +4,13 @@ namespace App\EventListener;
use App\Entity\Disposition; use App\Entity\Disposition;
use App\Entity\Upload; use App\Entity\Upload;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
{ {
public function __construct(private readonly TranslatorInterface $translator, private readonly Security $security) public function __construct(private readonly TranslatorInterface $translator)
{} {}
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array
@@ -41,11 +40,6 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
public function guardUploadContract(GuardEvent $event): void public function guardUploadContract(GuardEvent $event): void
{ {
// no checks for administrative users
if ($this->security->isGranted('ROLE_ADMINISTRATIVE')) {
return;
}
/** @var Disposition $disposition */ /** @var Disposition $disposition */
$disposition = $event->getSubject(); $disposition = $event->getSubject();
$assignment = $disposition->getAssignment(); $assignment = $disposition->getAssignment();
@@ -85,11 +79,6 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
$event->setBlocked(true, $message); $event->setBlocked(true, $message);
} }
// no further checks for administrative users
if ($this->security->isGranted('ROLE_ADMINISTRATIVE')) {
return;
}
$earliestDate = $destination->getDateTo(); $earliestDate = $destination->getDateTo();
if ($earliestDate > $today) { if ($earliestDate > $today) {
+16 -4
View File
@@ -5,6 +5,7 @@ namespace App\Security\Voter;
use App\BusProNet\DataProvider\HotelDataProvider; use App\BusProNet\DataProvider\HotelDataProvider;
use App\BusProNet\Model\Hotel; use App\BusProNet\Model\Hotel;
use App\Entity\Disposition; use App\Entity\Disposition;
use App\Entity\Upload;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -15,13 +16,12 @@ class DispositionVoter extends Voter
public const EDIT = 'EDIT'; public const EDIT = 'EDIT';
public const DELETE = 'DELETE'; public const DELETE = 'DELETE';
public const CONTRACT = 'CONTRACT'; public const CONTRACT = 'CONTRACT';
public const CONTRACT_SUPPLEMENTARY = 'CONTRACT_SUPPLEMENTARY';
public const INVOICE = 'INVOICE'; public const INVOICE = 'INVOICE';
public const FEEDBACK = 'FEEDBACK'; public const FEEDBACK = 'FEEDBACK';
public function __construct( public function __construct(private readonly Security $security)
private readonly Security $security, {
private readonly HotelDataProvider $hotelDataProvider
) {
} }
protected function supports(string $attribute, mixed $subject): bool protected function supports(string $attribute, mixed $subject): bool
@@ -35,6 +35,7 @@ class DispositionVoter extends Voter
static::EDIT, static::EDIT,
static::DELETE, static::DELETE,
static::CONTRACT, static::CONTRACT,
static::CONTRACT_SUPPLEMENTARY,
static::INVOICE, static::INVOICE,
static::FEEDBACK, static::FEEDBACK,
]); ]);
@@ -49,6 +50,7 @@ class DispositionVoter extends Voter
static::VIEW, static::EDIT, static::CONTRACT, static::INVOICE => $this->assertAdministrativeAccess() || $this->assertTeamerAccess($disposition), static::VIEW, static::EDIT, static::CONTRACT, static::INVOICE => $this->assertAdministrativeAccess() || $this->assertTeamerAccess($disposition),
static::DELETE => $this->assertAdminAccess(), static::DELETE => $this->assertAdminAccess(),
static::FEEDBACK => $this->assertHouseManagerAccess($token, $disposition), static::FEEDBACK => $this->assertHouseManagerAccess($token, $disposition),
static::CONTRACT_SUPPLEMENTARY => $this->assertContractUploadAllowed($disposition),
default => false, default => false,
}; };
} }
@@ -91,4 +93,14 @@ class DispositionVoter extends Voter
return $this->security->getUser()->getTeamer() === $disposition->getTeamer(); return $this->security->getUser()->getTeamer() === $disposition->getTeamer();
} }
private function assertContractUploadAllowed(Disposition $disposition): bool
{
if (false === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
return false;
}
return Disposition::STATUS_ENDED === $disposition->getStatus()
&& null === $disposition->getDocumentByType(Upload::TYPE_CONTRACT);
}
} }
@@ -179,7 +179,7 @@
</td> </td>
<td> <td>
<div class="flex items-center justify-end space-x-2"> <div class="flex items-center justify-end space-x-2">
{% if workflow_can(disposition, 'upload_contract') or workflow_can(disposition, 'upload_invoice') %} {% if is_granted('CONTRACT_SUPPLEMENTARY', disposition) %}
<button type="button" <button type="button"
title="Dokumentenupload" title="Dokumentenupload"
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid }) }}" hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid }) }}"
@@ -6,7 +6,7 @@
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }}) }} {{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }}) }}
<div class="pb-4"> <div class="pb-4">
{% include '_partials/_upload_collection_form.html.twig' with { {% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path(uploaderRoute), 'endpoint_upload': path('_uploader_upload_contract'),
'max_filesize': 5, 'max_filesize': 5,
'max_files': 1, 'max_files': 1,
'accepted_files': 'image/jpg,image/jpeg,application/pdf', 'accepted_files': 'image/jpg,image/jpeg,application/pdf',