diff --git a/composer.json b/composer.json
index b1dc0fd..3b9414d 100644
--- a/composer.json
+++ b/composer.json
@@ -55,6 +55,7 @@
"symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*",
"symfony/webpack-encore-bundle": "^2.0",
+ "symfony/workflow": "6.3.*",
"symfony/yaml": "6.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/html-extra": "^3.7",
diff --git a/composer.lock b/composer.lock
index 797fe09..0c34105 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "d714db6b49b6684caaf4544a90193da3",
+ "content-hash": "51c0d10a5e4bd074ddab5fe5717af560",
"packages": [
{
"name": "doctrine/cache",
@@ -8116,6 +8116,91 @@
],
"time": "2023-09-26T14:41:29+00:00"
},
+ {
+ "name": "symfony/workflow",
+ "version": "v6.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/workflow.git",
+ "reference": "a2a506b81556dea75a18f0d4a34462ff633e3522"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/workflow/zipball/a2a506b81556dea75a18f0d4a34462ff633e3522",
+ "reference": "a2a506b81556dea75a18f0d4a34462ff633e3522",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/event-dispatcher": "<5.4"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/event-dispatcher": "^5.4|^6.0",
+ "symfony/expression-language": "^5.4|^6.0",
+ "symfony/security-core": "^5.4|^6.0",
+ "symfony/validator": "^5.4|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Workflow\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools for managing a workflow or finite state machine",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "petrinet",
+ "place",
+ "state",
+ "statemachine",
+ "transition",
+ "workflow"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/workflow/tree/v6.3.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-08-01T15:55:06+00:00"
+ },
{
"name": "symfony/yaml",
"version": "v6.3.3",
diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml
index d3ad07b..d6ab107 100644
--- a/config/packages/oneup_uploader.yaml
+++ b/config/packages/oneup_uploader.yaml
@@ -18,6 +18,12 @@ oneup_uploader:
namer: app.upload_namer
storage:
directory: '%kernel.project_dir%/uploads/contract'
+ invoice:
+ frontend: dropzone
+ use_orphanage: true
+ namer: app.upload_namer
+ storage:
+ directory: '%kernel.project_dir%/uploads/invoice'
document:
frontend: dropzone
use_orphanage: true
diff --git a/config/packages/workflow.yaml b/config/packages/workflow.yaml
new file mode 100644
index 0000000..01eaa51
--- /dev/null
+++ b/config/packages/workflow.yaml
@@ -0,0 +1,33 @@
+framework:
+ workflows:
+ disposition:
+ type: state_machine
+ marking_store:
+ type: method
+ property: status
+ supports:
+ - App\Entity\Disposition
+ initial_marking: new
+ places:
+ - new
+ - checking_contract
+ - confirmed
+ - checking_invoice
+ - paid
+ - completed
+ transitions:
+ upload_contract:
+ from: new
+ to: checking_contract
+ confirm_contract:
+ from: checking_contract
+ to: confirmed
+ upload_invoice:
+ from: confirmed
+ to: checking_invoice
+ confirm_invoice:
+ from: checking_invoice
+ to: paid
+ complete:
+ from: confirmed
+ to: completed
diff --git a/migrations/Version20231012164550.php b/migrations/Version20231012164550.php
new file mode 100644
index 0000000..bcd8708
--- /dev/null
+++ b/migrations/Version20231012164550.php
@@ -0,0 +1,45 @@
+addSql('ALTER TABLE disposition DROP FOREIGN KEY FK_4C58BF60EA295441');
+ $this->addSql('ALTER TABLE disposition DROP FOREIGN KEY FK_4C58BF606D5F2C5F');
+ $this->addSql('DROP INDEX UNIQ_4C58BF606D5F2C5F ON disposition');
+ $this->addSql('DROP INDEX UNIQ_4C58BF60EA295441 ON disposition');
+ $this->addSql('ALTER TABLE disposition DROP contract_pdf_id, DROP invoice_pdf_id');
+ $this->addSql('ALTER TABLE upload ADD disposition_id INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE upload ADD CONSTRAINT FK_17BDE61F287B65ED FOREIGN KEY (disposition_id) REFERENCES disposition (id)');
+ $this->addSql('CREATE INDEX IDX_17BDE61F287B65ED ON upload (disposition_id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE disposition ADD contract_pdf_id INT DEFAULT NULL, ADD invoice_pdf_id INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE disposition ADD CONSTRAINT FK_4C58BF60EA295441 FOREIGN KEY (invoice_pdf_id) REFERENCES upload (id)');
+ $this->addSql('ALTER TABLE disposition ADD CONSTRAINT FK_4C58BF606D5F2C5F FOREIGN KEY (contract_pdf_id) REFERENCES upload (id)');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_4C58BF606D5F2C5F ON disposition (contract_pdf_id)');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_4C58BF60EA295441 ON disposition (invoice_pdf_id)');
+ $this->addSql('ALTER TABLE upload DROP FOREIGN KEY FK_17BDE61F287B65ED');
+ $this->addSql('DROP INDEX IDX_17BDE61F287B65ED ON upload');
+ $this->addSql('ALTER TABLE upload DROP disposition_id');
+ }
+}
diff --git a/src/Controller/Admin/Document/IndexController.php b/src/Controller/Admin/Document/IndexController.php
index 64a586e..a7f8048 100644
--- a/src/Controller/Admin/Document/IndexController.php
+++ b/src/Controller/Admin/Document/IndexController.php
@@ -22,14 +22,17 @@ class IndexController extends AbstractController
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): Response
{
- $query = $this->uploadRepository->getDocumentListQuery();
+ $query = $this
+ ->uploadRepository
+ ->getUploadListQuery()
+ ;
$pagination = $this->paginator->paginate(
$query,
$request->query->getInt('page', 1),
10,
[
- 'defaultSortFieldName' => 'upload.originalFilename',
+ 'defaultSortFieldName' => 'upload.createdAt',
'defaultSortDirection' => 'asc',
]
);
diff --git a/src/Controller/Admin/Document/DeleteController.php b/src/Controller/Admin/System/Document/DeleteController.php
similarity index 81%
rename from src/Controller/Admin/Document/DeleteController.php
rename to src/Controller/Admin/System/Document/DeleteController.php
index 545185a..a98eb41 100644
--- a/src/Controller/Admin/Document/DeleteController.php
+++ b/src/Controller/Admin/System/Document/DeleteController.php
@@ -1,6 +1,6 @@
$document->getOriginalFilename(),
]);
- return $this->redirectToRoute('app_admin_document_index');
+ return $this->redirectToRoute('app_admin_system_document_index');
}
}
\ No newline at end of file
diff --git a/src/Controller/Admin/System/Document/IndexController.php b/src/Controller/Admin/System/Document/IndexController.php
new file mode 100644
index 0000000..c48fea0
--- /dev/null
+++ b/src/Controller/Admin/System/Document/IndexController.php
@@ -0,0 +1,41 @@
+uploadRepository->getDocumentListQuery();
+
+ $pagination = $this->paginator->paginate(
+ $query,
+ $request->query->getInt('page', 1),
+ 10,
+ [
+ 'defaultSortFieldName' => 'upload.originalFilename',
+ 'defaultSortDirection' => 'asc',
+ ]
+ );
+
+ return $this->render('admin/system/document/index.html.twig', [
+ 'pagination' => $pagination,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/Admin/Document/RenameController.php b/src/Controller/Admin/System/Document/RenameController.php
similarity index 80%
rename from src/Controller/Admin/Document/RenameController.php
rename to src/Controller/Admin/System/Document/RenameController.php
index 149b953..13b0b90 100644
--- a/src/Controller/Admin/Document/RenameController.php
+++ b/src/Controller/Admin/System/Document/RenameController.php
@@ -1,6 +1,6 @@
generateUrl('app_admin_document_rename', ['uuid' => $upload->getUuid()]);
+ $formAction = $this->generateUrl('app_admin_system_document_rename', ['uuid' => $upload->getUuid()]);
$form = $this
->createFormBuilder($upload, ['action' => $formAction, 'ajax_submit' => true])
@@ -42,9 +42,9 @@ class RenameController extends AbstractController
$this->addFlash('success', 'Das Dokument wurden umbenannt');
$this->logger->info('Rename document');
- $response->setCloseAndRedirect($this->generateUrl('app_admin_document_index'));
+ $response->setCloseAndRedirect($this->generateUrl('app_admin_system_document_index'));
} else {
- $response->setContent($this->renderView('admin/document/rename.html.twig', [
+ $response->setContent($this->renderView('admin/system/document/rename.html.twig', [
'form' => $form,
]));
}
diff --git a/src/Controller/Admin/Document/ReplaceController.php b/src/Controller/Admin/System/Document/ReplaceController.php
similarity index 84%
rename from src/Controller/Admin/Document/ReplaceController.php
rename to src/Controller/Admin/System/Document/ReplaceController.php
index a39dd87..2ec8116 100644
--- a/src/Controller/Admin/Document/ReplaceController.php
+++ b/src/Controller/Admin/System/Document/ReplaceController.php
@@ -1,6 +1,6 @@
generateUrl('app_admin_document_replace', ['uuid' => $upload->getUuid()]);
+ $formAction = $this->generateUrl('app_admin_system_document_replace', ['uuid' => $upload->getUuid()]);
$form = $this
->createFormBuilder(null, ['action' => $formAction, 'ajax_submit' => true])
@@ -51,9 +51,9 @@ class ReplaceController extends AbstractController
$this->addFlash('success', 'Das Dokument wurden ersetzt');
$this->logger->info('Replace document');
- $response->setCloseAndRedirect($this->generateUrl('app_admin_document_index'));
+ $response->setCloseAndRedirect($this->generateUrl('app_admin_system_document_index'));
} else {
- $response->setContent($this->renderView('admin/document/replace.html.twig', [
+ $response->setContent($this->renderView('admin/system/document/replace.html.twig', [
'form' => $form,
]));
}
diff --git a/src/Controller/Admin/Document/UploadController.php b/src/Controller/Admin/System/Document/UploadController.php
similarity index 86%
rename from src/Controller/Admin/Document/UploadController.php
rename to src/Controller/Admin/System/Document/UploadController.php
index 0fee9e7..4904ea2 100644
--- a/src/Controller/Admin/Document/UploadController.php
+++ b/src/Controller/Admin/System/Document/UploadController.php
@@ -1,6 +1,6 @@
generateUrl('app_admin_document_upload');
+ $formAction = $this->generateUrl('app_admin_system_document_upload');
$form = $this
->createFormBuilder(null, ['action' => $formAction, 'ajax_submit' => true])
@@ -54,9 +54,9 @@ class UploadController extends AbstractController
$this->addFlash('success', 'Das/die Dokument/e wurden hochgeladen');
$this->logger->info('Upload document');
- $response->setCloseAndRedirect($this->generateUrl('app_admin_document_index'));
+ $response->setCloseAndRedirect($this->generateUrl('app_admin_system_document_index'));
} else {
- $response->setContent($this->renderView('admin/document/upload.html.twig', [
+ $response->setContent($this->renderView('admin/system/document/upload.html.twig', [
'form' => $form,
]));
}
diff --git a/src/Controller/Teamer/Disposition/ContractController.php b/src/Controller/Teamer/Disposition/ContractPdfController.php
similarity index 93%
rename from src/Controller/Teamer/Disposition/ContractController.php
rename to src/Controller/Teamer/Disposition/ContractPdfController.php
index 4ba1db2..db3c84c 100644
--- a/src/Controller/Teamer/Disposition/ContractController.php
+++ b/src/Controller/Teamer/Disposition/ContractPdfController.php
@@ -10,12 +10,12 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
-class ContractController extends AbstractController
+class ContractPdfController extends AbstractController
{
public function __construct(private readonly ContractRenderer $renderer)
{}
- #[Route('/teamer/disposition/contract/{uuid}', name: 'app_teamer_disposition_contract')]
+ #[Route('/teamer/disposition/contract/{uuid}', name: 'app_teamer_disposition_contractpdf')]
#[IsGranted('CONTRACT', subject: 'disposition')]
public function index(Disposition $disposition): Response
{
diff --git a/src/Controller/Teamer/Disposition/DetailController.php b/src/Controller/Teamer/Disposition/DetailController.php
index c0fad1e..40df4ca 100644
--- a/src/Controller/Teamer/Disposition/DetailController.php
+++ b/src/Controller/Teamer/Disposition/DetailController.php
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
+use Symfony\Component\Workflow\WorkflowInterface;
class DetailController extends AbstractController
{
@@ -23,6 +24,7 @@ class DetailController extends AbstractController
public function __construct(
private readonly UploadHandler $uploadHandler,
private readonly EntityManagerInterface $entityManager,
+ private readonly WorkflowInterface $dispositionStateMachine,
private readonly LoggerInterface $logger
) {
}
@@ -31,16 +33,22 @@ class DetailController extends AbstractController
#[IsGranted('VIEW', subject: 'disposition')]
public function index(Disposition $disposition, Request $request): Response
{
- // Handle upload independently from form submission to avoid issues with failing validation
- $uploadSession = $this->uploadHandler->getUploadSession();
- if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) {
- $this->updateContract($disposition, $uploadSession);
- }
-
+ // Create dummy form without fields to generate POST requests
$form = $this->createFormBuilder()->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
+ $uploadSession = $this->uploadHandler->getUploadSession();
+
+ if (0 < $uploadSession->getCount()) {
+ if (true === $this->dispositionStateMachine->can($disposition, 'upload_contract')) {
+ $this->uploadContract($disposition, $uploadSession);
+ }
+ if (true === $this->dispositionStateMachine->can($disposition, 'upload_invoice')) {
+ $this->uploadInvoice($disposition, $uploadSession);
+ }
+ }
+
return $this->redirectToRoute('app_teamer_disposition_detail', [
'uuid' => $disposition->getUuid(),
]);
@@ -48,28 +56,61 @@ class DetailController extends AbstractController
return $this->render('teamer/disposition/detail.html.twig', [
'disposition' => $disposition,
- 'returnUrl' => $this->getReturnUrl($request, 'app_teamer_index'),
'form' => $form,
+ 'returnUrl' => $this->getReturnUrl($request, 'app_teamer_index'),
]);
}
-
- private function updateContract(Disposition $disposition, UploadSessionDto $uploadSession): void
+ private function uploadContract(Disposition $disposition, UploadSessionDto $uploadSession): void
{
- if (null !== $existingUpload = $disposition->getContractPdf()) {
+ if (null !== $existingUpload = $disposition->getDocumentByType(Upload::TYPE_CONTRACT)) {
$this->entityManager->remove($existingUpload);
}
/** @var User $user */
$user = $this->getUser();
+
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_CONTRACT);
- $disposition->setContractPdf($upload);
+ $upload->setStatus(Upload::STATUS_NEW);
+
+ $disposition->addDocument($upload);
+ $this->dispositionStateMachine->apply($disposition, 'upload_contract');
+
$this->entityManager->flush();
+
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_CONTRACT, $uploadSession);
$this->uploadHandler->destroyUploadSession();
$this->addFlash('success', 'Der Honorarvertrag wurde hochgeladen');
- $this->logger->info('Update contract', [
+
+ $this->logger->info('Upload contract', [
+ 'user' => $user->getUserIdentifier(),
+ ]);
+ }
+
+ private function uploadInvoice(Disposition $disposition, UploadSessionDto $uploadSession): void
+ {
+ if (null !== $existingUpload = $disposition->getDocumentByType(Upload::TYPE_INVOICE)) {
+ $this->entityManager->remove($existingUpload);
+ }
+
+ /** @var User $user */
+ $user = $this->getUser();
+
+ $upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_INVOICE);
+ $upload->setStatus(Upload::STATUS_NEW);
+
+ $disposition->addDocument($upload);
+ $this->dispositionStateMachine->apply($disposition, 'upload_invoice');
+
+ $this->entityManager->flush();
+
+ $this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_INVOICE, $uploadSession);
+ $this->uploadHandler->destroyUploadSession();
+
+ $this->addFlash('success', 'Die Honornote wurde hochgeladen');
+
+ $this->logger->info('Upload invoice', [
'user' => $user->getUserIdentifier(),
]);
}
diff --git a/src/Entity/Disposition.php b/src/Entity/Disposition.php
index e1079f1..de56ed0 100644
--- a/src/Entity/Disposition.php
+++ b/src/Entity/Disposition.php
@@ -5,6 +5,8 @@ namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\DispositionRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
@@ -16,8 +18,11 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
use TimestampableEntity;
public const STATUS_NEW = 'new';
- public const STATUS_PENDING = 'pending';
+ public const STATUS_CHECKING_CONTRACT = 'checking_contract';
public const STATUS_CONFIRMED = 'confirmed';
+ public const STATUS_CHECKING_INVOICE = 'checking_invoice';
+ public const STATUS_PAID = 'paid';
+ public const STATUS_COMPLETED = 'completed';
#[ORM\Id]
#[ORM\GeneratedValue]
@@ -39,11 +44,8 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $remarks;
- #[ORM\OneToOne(cascade: ['persist', 'remove'])]
- private ?Upload $contractPdf = null;
-
- #[ORM\OneToOne(cascade: ['persist', 'remove'])]
- private ?Upload $invoicePdf = null;
+ #[ORM\OneToMany(mappedBy: 'disposition', targetEntity: Upload::class, cascade: ['persist', 'remove'])]
+ private Collection $documents;
public function __construct(Application $application)
{
@@ -52,6 +54,7 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
$this->assignment = $application->getAssignment();
$this->teamer = $application->getTeamer();
$this->remarks = $application->getRemarks();
+ $this->documents = new ArrayCollection();
}
public function getId(): ?int
@@ -112,26 +115,43 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
return $this;
}
- public function getContractPdf(): ?Upload
+ /**
+ * @return Collection
Dokumentenübersicht
-
-
-
+
-
-
-
- {% for document in pagination %}
+
- {{ knp_pagination_sortable(pagination, 'Dateiname', 'upload.originalFilename') }}
-
-
- Dateigröße
-
-
- Upload am
-
-
- Upload von
-
-
-
+
+
+
+ {% for upload in pagination %}
+ {% set disposition = upload.disposition %}
+ {% set assignment = disposition.assignment %}
+
-
- {% else %}
-
- {{ document.displayName ? document.displayName : document.originalFilename }}
-
-
- {{ document.size|file_size }}
-
-
- {{ document.createdAt|date('d.m.Y') }}
-
-
- {{ document.createdBy }}
-
-
-
+
+ {{ knp_pagination_sortable(pagination, 'Einsatzzeitraum', 'destination.dateFrom') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Jobprofil', 'job_profile.name') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Destination', 'destination.product') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Dokument', 'upload.type') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Teamer', 'teamer.lastName') }}
+
+
+ {{ knp_pagination_sortable(pagination, 'Status', 'disposition.status') }}
+
+
-
- {% endfor %}
-
-
- Keine Daten...
-
-
+
+ {% else %}
+
+ {{ assignment.destination.dateFrom|date('d.m.Y') }}
+ -
+ {{ assignment.destination.dateTo|date('d.m.Y') }}
+
+
+ {{ assignment.jobProfile.name }}
+
+
+ {{ assignment.destination.product }}
+
+
+ {{ assignment.destination.hotel }}
+
+ {{ ('label.document.type.' ~ upload.type)|trans }}
+
+
+ {{ disposition.teamer }}
+
+
+ {{ ('label.document.status.' ~ upload.status)|trans }}
+
+
+
+
+
+
+ {% endfor %}
+
+
+ Keine Daten...
+
+
+ Dokumentenübersicht
+
+
+
+
+
+
+
+
+ {% for document in pagination %}
+
+ {{ knp_pagination_sortable(pagination, 'Dateiname', 'upload.originalFilename') }}
+
+
+ Dateigröße
+
+
+ Upload am
+
+
+ Upload von
+
+
+
+
+ {% else %}
+
+ {{ document.displayName ? document.displayName : document.originalFilename }}
+
+
+ {{ document.size|file_size }}
+
+
+ {{ document.createdAt|date('d.m.Y') }}
+
+
+ {{ document.createdBy }}
+
+
+
+
+
+ {% endfor %}
+
+
+ Keine Daten...
+
+
{{ assignment.contact }}, {{ assignment.contact.email }}
- Bitte lade ihn unterschrieben hier wieder hoch: -
- {{ form_start(form) }} -+ Bitte lade ihn unterschrieben hier wieder hoch: +
+ {{ form_start(form) }} ++ Bitte lade sie ausgefüllt und unterschrieben hier wieder hoch: +
+ {{ form_start(form) }} +