diff --git a/assets/images/contract_thumb.jpg b/assets/images/contract_thumb.jpg new file mode 100644 index 0000000..22aeb7b Binary files /dev/null and b/assets/images/contract_thumb.jpg differ diff --git a/assets/pdf/contract.pdf b/assets/pdf/contract.pdf new file mode 100644 index 0000000..0e6ee8c Binary files /dev/null and b/assets/pdf/contract.pdf differ diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml index c6d58e2..058a3f8 100644 --- a/config/packages/oneup_uploader.yaml +++ b/config/packages/oneup_uploader.yaml @@ -12,6 +12,12 @@ oneup_uploader: namer: app.upload_namer storage: directory: '%kernel.project_dir%/uploads/certificate' + contract: + frontend: dropzone + use_orphanage: true + namer: app.upload_namer + storage: + directory: '%kernel.project_dir%/uploads/contract' chunks: maxage: 86400 storage: diff --git a/config/services.yaml b/config/services.yaml index dc0daa2..a40437b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -106,3 +106,6 @@ services: class: App\Service\Upload\UploadNamer public: true + App\Service\Pdf\ContractRenderer: + arguments: + $options: { 'project_dir': '%kernel.project_dir%' } diff --git a/src/Controller/Teamer/Disposition/ContractController.php b/src/Controller/Teamer/Disposition/ContractController.php new file mode 100644 index 0000000..4ba1db2 --- /dev/null +++ b/src/Controller/Teamer/Disposition/ContractController.php @@ -0,0 +1,36 @@ +renderer->render($disposition); + + $teamer = $disposition->getTeamer(); + + $filename = sprintf('Honorarvertrag_%s_%s.pdf', $teamer, $disposition->getAssignment()->getDestination()); + $disposition = HeaderUtils::makeDisposition(HeaderUtils::DISPOSITION_ATTACHMENT, $filename, md5($filename)); + + $response = new Response($pdf->Output('S')); + $response->setPrivate(); + $response->headers->set('Content-type', 'application/pdf'); + $response->headers->set('Content-Disposition', $disposition); + + return $response; + } +} \ No newline at end of file diff --git a/src/Controller/Teamer/Disposition/DetailController.php b/src/Controller/Teamer/Disposition/DetailController.php index c84f300..c0fad1e 100644 --- a/src/Controller/Teamer/Disposition/DetailController.php +++ b/src/Controller/Teamer/Disposition/DetailController.php @@ -4,6 +4,12 @@ namespace App\Controller\Teamer\Disposition; use App\Controller\Traits\ReturnUrlTrait; use App\Entity\Disposition; +use App\Entity\Upload; +use App\Entity\User; +use App\Model\UploadSessionDto; +use App\Service\Upload\UploadHandler; +use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -14,13 +20,57 @@ class DetailController extends AbstractController { use ReturnUrlTrait; + public function __construct( + private readonly UploadHandler $uploadHandler, + private readonly EntityManagerInterface $entityManager, + private readonly LoggerInterface $logger + ) { + } + #[Route('/teamer/disposition/detail/{uuid}', name: 'app_teamer_disposition_detail')] #[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); + } + + $form = $this->createFormBuilder()->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + return $this->redirectToRoute('app_teamer_disposition_detail', [ + 'uuid' => $disposition->getUuid(), + ]); + } + return $this->render('teamer/disposition/detail.html.twig', [ 'disposition' => $disposition, 'returnUrl' => $this->getReturnUrl($request, 'app_teamer_index'), + 'form' => $form, + ]); + } + + + private function updateContract(Disposition $disposition, UploadSessionDto $uploadSession): void + { + if (null !== $existingUpload = $disposition->getContractPdf()) { + $this->entityManager->remove($existingUpload); + } + + /** @var User $user */ + $user = $this->getUser(); + $upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_CONTRACT); + $disposition->setContractPdf($upload); + $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', [ + 'user' => $user->getUserIdentifier(), ]); } } \ No newline at end of file diff --git a/src/Entity/Embeddable/Address.php b/src/Entity/Embeddable/Address.php index 4193bd4..7e4a0bd 100644 --- a/src/Entity/Embeddable/Address.php +++ b/src/Entity/Embeddable/Address.php @@ -52,6 +52,11 @@ class Address } } + public function __toString() + { + return sprintf('%s, %s %s', $this->street, $this->postCode, $this->city); + } + public function toPayload(): array { return [ diff --git a/src/Security/Voter/DispositionVoter.php b/src/Security/Voter/DispositionVoter.php index 1629196..cc3dbeb 100644 --- a/src/Security/Voter/DispositionVoter.php +++ b/src/Security/Voter/DispositionVoter.php @@ -4,7 +4,6 @@ namespace App\Security\Voter; use App\Entity\Disposition; use App\Entity\User; -use App\Repository\ApplicationRepository; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -13,9 +12,7 @@ class DispositionVoter extends Voter public const VIEW = 'VIEW'; public const EDIT = 'EDIT'; public const DELETE = 'DELETE'; - - public function __construct(private readonly ApplicationRepository $applicationRepository) - {} + public const CONTRACT = 'CONTRACT'; protected function supports(string $attribute, mixed $subject): bool { @@ -23,7 +20,7 @@ class DispositionVoter extends Voter return false; } - return in_array($attribute, [static::VIEW, static::EDIT, static::DELETE]); + return in_array($attribute, [static::VIEW, static::EDIT, static::DELETE, static::CONTRACT]); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool @@ -34,7 +31,7 @@ class DispositionVoter extends Voter } // Teamers may only view or edit their own dispositions - if (in_array('ROLE_TEAMER', $token->getRoleNames()) && in_array($attribute, [static::VIEW, static::EDIT])) { + if (in_array('ROLE_TEAMER', $token->getRoleNames()) && in_array($attribute, [static::VIEW, static::EDIT, static::CONTRACT])) { /** @var User $user */ $user = $token->getUser(); $teamer = $user->getTeamer(); diff --git a/src/Service/Pdf/AbstractPdfRenderer.php b/src/Service/Pdf/AbstractPdfRenderer.php new file mode 100644 index 0000000..26e68ab --- /dev/null +++ b/src/Service/Pdf/AbstractPdfRenderer.php @@ -0,0 +1,32 @@ +setRequired('project_dir') + ->setAllowedTypes('project_dir', 'string') + ; + $this->config = $optionsResolver->resolve($options); + + // Static cache without serialization + $this->cache = new ArrayAdapter(0, false); + } +} diff --git a/src/Service/Pdf/ContractRenderer.php b/src/Service/Pdf/ContractRenderer.php new file mode 100644 index 0000000..9465ee3 --- /dev/null +++ b/src/Service/Pdf/ContractRenderer.php @@ -0,0 +1,54 @@ +SetAuthor('Continentale', true); + $pdf->SetCreator('Continentale', true); + + // Load template + $pdfTemplate = sprintf('%s/assets/pdf/contract.pdf', $this->config['project_dir']); + $numberOfPages = $pdf->appendPdf($pdfTemplate); + $pdf->setPage(1); + + $pdf->SetFont('Arial', '', 10); + + $teamer = $disposition->getTeamer(); + $assignment = $disposition->getAssignment(); + $destination = $assignment->getDestination(); + + // Current date + $date = (new \DateTimeImmutable())->format('d.m.Y'); + $pdf->Text(164,43, utf8_decode($date)); + + // Name + $pdf->Text(45,67.75, utf8_decode($teamer)); + + // Address + $pdf->Text(45,77.75, utf8_decode($teamer->getAddress())); + + // Jobprofile + $pdf->Text(21,104.5, utf8_decode($assignment->getJobProfile()->getName())); + + // Period + $dateFrom = $assignment->getTotalPeriod()->start->format('d.m.Y'); + $dateTo = $assignment->getTotalPeriod()->end->format('d.m.Y'); + $period = sprintf('%s - %s', $dateFrom, $dateTo); + $pdf->Text(57,113, $period); + + // Destination + $pdf->Text(57,121.5, utf8_decode(sprintf('%s, %s', $destination->getProduct(), $destination->getHotel()))); + + // Set pointer to last page to include all + $pdf->setPage($numberOfPages); + + return $pdf; + } +} diff --git a/src/Service/Pdf/Pdf.php b/src/Service/Pdf/Pdf.php new file mode 100644 index 0000000..45eff45 --- /dev/null +++ b/src/Service/Pdf/Pdf.php @@ -0,0 +1,120 @@ +page = $pageNumber; + } + + public function appendPdf(string $sourceFile): int + { + try { + $pageCount = $this->setSourceFile($sourceFile); + for ($page = 1; $page <= $pageCount; ++$page) { + $templateId = $this->importPage($page); + $size = $this->getTemplateSize($templateId); + $this->AddPage($size['orientation'], [$size['width'], $size['height']]); + $this->useTemplate($templateId); + } + + return $pageCount; + } catch (PdfParserException|PdfReaderException $e) { + $this->AddPage(); + $this->SetFont('Arial', '', 10); + $this->Text(self::MARGIN_X, self::MARGIN_Y + 10, 'PDF-Fehler: '.$e->getMessage()); + + return 1; + } + } + + public function appendImage(string $sourceFile, string $pageTitle = ''): void + { + $this->AddPage(); + $containerWidth = self::A4_WIDTH - self::MARGIN_X * 2; + $containerHeight = self::A4_HEIGHT - self::MARGIN_Y * 3; + $this->centerImage( + $sourceFile, + self::MARGIN_X, + self::MARGIN_Y * 2, + $containerWidth, + $containerHeight + ); + + if ($pageTitle) { + $this->SetFont('Arial', '', 10); + $this->Text(self::MARGIN_X, self::MARGIN_Y, utf8_decode($pageTitle)); + } + } + + public function centerImage( + string $imgPath, + int $x = 0, + int $y = 0, + int $containerWidth = self::A4_WIDTH, + int $containerHeight = self::A4_HEIGHT + ): void { + [$width, $height] = $this->resizeToFit($imgPath, $containerWidth, $containerHeight); + + try { + $this->Image( + $imgPath, + $x + ($containerWidth - $width) / 2, + $y + ($containerHeight - $height) / 2, + $width, + $height + ); + } catch (\Exception $e) { + $this->SetFont('Arial', '', 10); + $this->Text(self::MARGIN_X, self::MARGIN_Y + 10, 'PDF-Fehler: '.$e->getMessage()); + } + } + + protected function pixelsToMm($val): int + { + return (int) (round($val * $this::MM_IN_INCH / self::DPI)); + } + + protected function mmToPixels($val): int + { + return (int) (round($this::DPI * $val / self::MM_IN_INCH)); + } + + protected function resizeToFit( + string $imgPath, + int $maxWidth = self::A4_WIDTH, + int $maxHeight = self::A4_HEIGHT + ): array { + [$width, $height] = getimagesize($imgPath); + $widthScale = $this->mmToPixels($maxWidth) / $width; + $heightScale = $this->mmToPixels($maxHeight) / $height; + $scale = min($widthScale, $heightScale); + + return [ + $this->pixelsToMm($scale * $width), + $this->pixelsToMm($scale * $height), + ]; + } +} diff --git a/templates/teamer/disposition/detail.html.twig b/templates/teamer/disposition/detail.html.twig index 8835ccd..3002598 100644 --- a/templates/teamer/disposition/detail.html.twig +++ b/templates/teamer/disposition/detail.html.twig @@ -6,7 +6,7 @@
+ Bitte lade ihn unterschrieben hier wieder hoch: +
+ {{ form_start(form) }} +