feat: enable document uploads for admins on behalf of teamer

closes #869btf5bm
This commit is contained in:
Björn Fromme
2026-03-15 12:42:28 +01:00
parent 2195467f50
commit 6ec9fa6f9d
4 changed files with 109 additions and 62 deletions
@@ -31,22 +31,33 @@ class DocumentUploadController extends AbstractController
} }
#[Route( #[Route(
path: '/administrative/disposition/document-upload/{uuid}', path: '/administrative/disposition/document-upload/{uuid}/{type}',
name: 'app_administrative_disposition_document_upload' name: 'app_administrative_disposition_document_upload'
)] )]
#[IsGranted('CONTRACT_SUPPLEMENTARY', subject: 'disposition')] #[IsGranted('ADMIN_DOCUMENT_UPLOAD', subject: 'disposition')]
public function index(Disposition $disposition, Request $request): Response public function index(Disposition $disposition, string $type, Request $request): Response
{ {
if (false === in_array($type, [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE], true)) {
throw $this->createNotFoundException('Unsupported document type');
}
$teamer = $disposition->getTeamer(); $teamer = $disposition->getTeamer();
$modalTitle = sprintf('Honorarvertrag für %s hochladen', $teamer); $modalTitle = Upload::TYPE_CONTRACT === $type
? sprintf('Honorarvertrag für %s hochladen', $teamer)
: sprintf('Honorarnote 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); $this->processUpload($disposition, $type);
$this->addFlash('success', 'Der Honorarvertrag wurde hochgeladen'); $this->addFlash(
'success',
Upload::TYPE_CONTRACT === $type
? 'Der Honorarvertrag wurde hochgeladen'
: 'Die Honorarnote wurde hochgeladen'
);
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [ return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
'uuid' => $disposition->getAssignment()->getUuid(), 'uuid' => $disposition->getAssignment()->getUuid(),
@@ -56,13 +67,16 @@ class DocumentUploadController extends AbstractController
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,
'endpointUpload' => Upload::TYPE_CONTRACT === $type
? '_uploader_upload_contract'
: '_uploader_upload_invoice',
'form' => $form->createView(), 'form' => $form->createView(),
]); ]);
} }
private function processUpload(Disposition $disposition): void private function processUpload(Disposition $disposition, string $type): void
{ {
if (null !== $existingUpload = $disposition->getDocumentByType(Upload::TYPE_CONTRACT)) { if (null !== $existingUpload = $disposition->getDocumentByType($type)) {
$this->entityManager->remove($existingUpload); $this->entityManager->remove($existingUpload);
} }
@@ -76,23 +90,28 @@ class DocumentUploadController extends AbstractController
$upload = Upload::fromUploadDto( $upload = Upload::fromUploadDto(
$uploadSession->getUploads()->first(), $uploadSession->getUploads()->first(),
$teamer->getUser(), $teamer->getUser(),
Upload::TYPE_CONTRACT $type
); );
if (Upload::TYPE_CONTRACT === $type) {
$upload $upload
->setStatus(Upload::STATUS_CHECKED) ->setStatus(Upload::STATUS_CHECKED)
->setApprovedAt(new \DateTimeImmutable()) ->setApprovedAt(new \DateTimeImmutable())
->setApprovedBy($user->getInitials()) ->setApprovedBy($user->getInitials())
; ;
$disposition $disposition->setStatus(Disposition::STATUS_CONFIRMED);
->addDocument($upload) } else {
->setStatus(Disposition::STATUS_CONFIRMED) $upload->setStatus(Upload::STATUS_NEW);
;
$disposition->setStatus(Disposition::STATUS_CHECKING_INVOICE);
}
$disposition->addDocument($upload);
$this->entityManager->flush(); $this->entityManager->flush();
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_CONTRACT, $uploadSession); $this->uploadHandler->moveUploadSessionFilesFromOrphanage($type, $uploadSession);
$this->uploadHandler->destroyUploadSession(); $this->uploadHandler->destroyUploadSession();
if (true === $this->featureManager->isActive('sanitize_uploads')) { if (true === $this->featureManager->isActive('sanitize_uploads')) {
@@ -100,14 +119,16 @@ class DocumentUploadController extends AbstractController
} }
$this->eventDispatcher->dispatch(new DocumentUploadedEvent($upload), DocumentUploadedEvent::NAME); $this->eventDispatcher->dispatch(new DocumentUploadedEvent($upload), DocumentUploadedEvent::NAME);
if (Upload::TYPE_CONTRACT === $type) {
$this->eventDispatcher->dispatch(new DocumentConfirmedEvent($upload), DocumentConfirmedEvent::NAME); $this->eventDispatcher->dispatch(new DocumentConfirmedEvent($upload), DocumentConfirmedEvent::NAME);
}
$this->logger->info('Document upload for teamer', [ $this->logger->info('Administrative document upload for teamer', [
'teamer_id' => $teamer->getId(), 'teamer_id' => $teamer->getId(),
'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' => Upload::TYPE_CONTRACT, 'upload_type' => $type,
]); ]);
} }
} }
+13
View File
@@ -16,6 +16,7 @@ class DispositionVoter extends Voter
public const DELETE = 'DELETE'; public const DELETE = 'DELETE';
public const CONTRACT = 'CONTRACT'; public const CONTRACT = 'CONTRACT';
public const CONTRACT_SUPPLEMENTARY = 'CONTRACT_SUPPLEMENTARY'; public const CONTRACT_SUPPLEMENTARY = 'CONTRACT_SUPPLEMENTARY';
public const ADMIN_DOCUMENT_UPLOAD = 'ADMIN_DOCUMENT_UPLOAD';
public const INVOICE = 'INVOICE'; public const INVOICE = 'INVOICE';
public const FEEDBACK = 'FEEDBACK'; public const FEEDBACK = 'FEEDBACK';
public const CALL_OFF = 'CALL_OFF'; public const CALL_OFF = 'CALL_OFF';
@@ -36,6 +37,7 @@ class DispositionVoter extends Voter
static::DELETE, static::DELETE,
static::CONTRACT, static::CONTRACT,
static::CONTRACT_SUPPLEMENTARY, static::CONTRACT_SUPPLEMENTARY,
static::ADMIN_DOCUMENT_UPLOAD,
static::INVOICE, static::INVOICE,
static::FEEDBACK, static::FEEDBACK,
static::CALL_OFF, static::CALL_OFF,
@@ -54,6 +56,7 @@ class DispositionVoter extends Voter
static::FEEDBACK => $this->security->isGranted('ROLE_ADMINISTRATIVE') static::FEEDBACK => $this->security->isGranted('ROLE_ADMINISTRATIVE')
|| $this->assertHouseManagerAccess($token, $disposition), || $this->assertHouseManagerAccess($token, $disposition),
static::CONTRACT_SUPPLEMENTARY => $this->assertContractUploadAllowed($disposition), static::CONTRACT_SUPPLEMENTARY => $this->assertContractUploadAllowed($disposition),
static::ADMIN_DOCUMENT_UPLOAD => $this->assertAdminDocumentUploadAllowed($disposition),
static::CALL_OFF => $this->security->isGranted('ROLE_ADMINISTRATIVE') static::CALL_OFF => $this->security->isGranted('ROLE_ADMINISTRATIVE')
&& Disposition::STATUS_CALLED_OFF !== $disposition->getStatus(), && Disposition::STATUS_CALLED_OFF !== $disposition->getStatus(),
default => false, default => false,
@@ -110,4 +113,14 @@ class DispositionVoter extends Voter
return null === $existingContract || Upload::STATUS_REJECTED === $existingContract->getStatus(); return null === $existingContract || Upload::STATUS_REJECTED === $existingContract->getStatus();
} }
private function assertAdminDocumentUploadAllowed(Disposition $disposition): bool
{
if (false === $this->security->isGranted('ROLE_ADMINISTRATIVE')
&& false === $this->security->isGranted('ROLE_ADMIN')) {
return false;
}
return Disposition::STATUS_CALLED_OFF !== $disposition->getStatus();
}
} }
@@ -177,53 +177,66 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<div class="flex items-start space-x-2">
<div class="flex-1">
{{ disposition.specialAgreements|default('-')|nl2br }} {{ disposition.specialAgreements|default('-')|nl2br }}
</div>
<button type="button"
title="zusätzliche Absprachen {{ disposition.teamer }}"
hx-get="{{ path('app_administrative_disposition_special_agreements', { 'uuid': disposition.uuid }) }}"
hx-target="body"
hx-swap="beforeend">
{{ icon('edit') }}
</button>
</div>
</td> </td>
<td> <td>
<div class="flex items-center justify-end space-x-2"> <div class="flex items-center justify-end space-x-2">
<button type="button" hx-post="{{ path('app_administrative_disposition_bpn_sync_toggle', { 'uuid': disposition.uuid }) }}" hx-swap="none"> <button type="button" hx-post="{{ path('app_administrative_disposition_bpn_sync_toggle', { 'uuid': disposition.uuid }) }}" hx-swap="none">
{{ icon('chair', html_classes('w-5 h-5', {'text-green-500': disposition.syncedWithBusProNet })) }} {{ icon('chair', html_classes('w-5 h-5', {'text-green-500': disposition.syncedWithBusProNet })) }}
</button> </button>
{% if is_granted('CONTRACT_SUPPLEMENTARY', disposition) %} <twig:DropDown>
<button type="button" <button type="button"
title="Dokumentenupload" class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left"
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid }) }}" role="menuitem"
tabindex="-1"
hx-get="{{ path('app_administrative_disposition_special_agreements', { 'uuid': disposition.uuid }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
{{ icon('upload') }} zusätzliche Absprachen
</button>
{% if is_granted('ADMIN_DOCUMENT_UPLOAD', disposition) %}
<button type="button"
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid, 'type': 'contract' }) }}"
hx-target="body"
hx-swap="beforeend">
Honorarvertrag hochladen
</button>
<button type="button"
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm w-full text-left"
role="menuitem"
tabindex="-1"
hx-get="{{ path('app_administrative_disposition_document_upload', { 'uuid': disposition.uuid, 'type': 'invoice' }) }}"
hx-target="body"
hx-swap="beforeend">
Honorarnote hochladen
</button> </button>
{% endif %} {% endif %}
{% if is_granted('CALL_OFF', disposition) %} {% if is_granted('CALL_OFF', disposition) %}
<button type="button" <button type="button"
title="Absage" class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
class="text-red-500" role="menuitem"
tabindex="-1"
hx-get="{{ path('app_administrative_disposition_call_off', { 'uuid': disposition.uuid }) }}" hx-get="{{ path('app_administrative_disposition_call_off', { 'uuid': disposition.uuid }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
{{ icon('cancel') }} absagen
</button> </button>
{% endif %} {% endif %}
{% if is_granted('DELETE', disposition) %} {% if is_granted('DELETE', disposition) %}
<button type="button" <button type="button"
class="text-red-500" class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
title="Einteilung löschen" role="menuitem"
tabindex="-1"
hx-get="{{ path('app_admin_disposition_delete', { 'uuid': disposition.uuid }) }}" hx-get="{{ path('app_admin_disposition_delete', { 'uuid': disposition.uuid }) }}"
hx-target="body" hx-target="body"
hx-swap="beforeend"> hx-swap="beforeend">
{{ icon('delete') }} löschen
</button> </button>
{% endif %} {% endif %}
</twig:DropDown>
</div> </div>
</td> </td>
</tr> </tr>
@@ -7,7 +7,7 @@
{{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }|merge(formAttr.toArray()) }) }} {{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' }|merge(formAttr.toArray()) }) }}
<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('_uploader_upload_contract'), 'endpoint_upload': path(endpointUpload),
'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',