From 46915955b0c416a82b25e4aa15cafc3881505be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 12 Oct 2023 10:41:31 +0200 Subject: [PATCH] WIP: Implement document handling --- config/packages/oneup_uploader.yaml | 6 ++ migrations/Version20231012074633.php | 35 ++++++++ migrations/Version20231012082751.php | 31 +++++++ ...ntroller.php => DestinationController.php} | 2 +- .../Admin/Autocomplete/DocumentController.php | 32 +++++++ .../Admin/Document/DeleteController.php | 36 ++++++++ .../Admin/Document/IndexController.php | 27 +++++- .../Admin/Document/ReplaceController.php | 63 +++++++++++++ .../Admin/Document/UploadController.php | 66 ++++++++++++++ src/Entity/Assignment.php | 28 ++++++ src/Entity/Upload.php | 20 ++++- src/Form/AssignmentType.php | 15 ++++ src/Repository/UploadRepository.php | 73 ++++++++------- templates/admin/assignment/_form.html.twig | 28 ++++++ templates/admin/document/index.html.twig | 88 +++++++++++++++++-- templates/admin/document/replace.html.twig | 14 +++ templates/admin/document/upload.html.twig | 13 +++ templates/teamer/disposition/detail.html.twig | 1 - 18 files changed, 529 insertions(+), 49 deletions(-) create mode 100644 migrations/Version20231012074633.php create mode 100644 migrations/Version20231012082751.php rename src/Controller/Admin/Autocomplete/{BpnDateController.php => DestinationController.php} (94%) create mode 100644 src/Controller/Admin/Autocomplete/DocumentController.php create mode 100644 src/Controller/Admin/Document/DeleteController.php create mode 100644 src/Controller/Admin/Document/ReplaceController.php create mode 100644 src/Controller/Admin/Document/UploadController.php create mode 100644 templates/admin/document/replace.html.twig create mode 100644 templates/admin/document/upload.html.twig diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml index 058a3f8..d3ad07b 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' + document: + frontend: dropzone + use_orphanage: true + namer: app.upload_namer + storage: + directory: '%kernel.project_dir%/uploads/document' chunks: maxage: 86400 storage: diff --git a/migrations/Version20231012074633.php b/migrations/Version20231012074633.php new file mode 100644 index 0000000..c020996 --- /dev/null +++ b/migrations/Version20231012074633.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE assignment_upload (assignment_id INT NOT NULL, upload_id INT NOT NULL, INDEX IDX_2F9CC602D19302F8 (assignment_id), INDEX IDX_2F9CC602CCCFBA31 (upload_id), PRIMARY KEY(assignment_id, upload_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE assignment_upload ADD CONSTRAINT FK_2F9CC602D19302F8 FOREIGN KEY (assignment_id) REFERENCES assignment (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE assignment_upload ADD CONSTRAINT FK_2F9CC602CCCFBA31 FOREIGN KEY (upload_id) REFERENCES upload (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE assignment_upload DROP FOREIGN KEY FK_2F9CC602D19302F8'); + $this->addSql('ALTER TABLE assignment_upload DROP FOREIGN KEY FK_2F9CC602CCCFBA31'); + $this->addSql('DROP TABLE assignment_upload'); + } +} diff --git a/migrations/Version20231012082751.php b/migrations/Version20231012082751.php new file mode 100644 index 0000000..2b3af64 --- /dev/null +++ b/migrations/Version20231012082751.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE upload ADD display_name VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE upload DROP display_name'); + } +} diff --git a/src/Controller/Admin/Autocomplete/BpnDateController.php b/src/Controller/Admin/Autocomplete/DestinationController.php similarity index 94% rename from src/Controller/Admin/Autocomplete/BpnDateController.php rename to src/Controller/Admin/Autocomplete/DestinationController.php index c9c4384..384d065 100644 --- a/src/Controller/Admin/Autocomplete/BpnDateController.php +++ b/src/Controller/Admin/Autocomplete/DestinationController.php @@ -9,7 +9,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -class BpnDateController extends AbstractController +class DestinationController extends AbstractController { public function __construct(private readonly DestinationRepository $bpnDateRepository) {} diff --git a/src/Controller/Admin/Autocomplete/DocumentController.php b/src/Controller/Admin/Autocomplete/DocumentController.php new file mode 100644 index 0000000..0e68b51 --- /dev/null +++ b/src/Controller/Admin/Autocomplete/DocumentController.php @@ -0,0 +1,32 @@ +getContent(), true, 512, JSON_THROW_ON_ERROR); + $queryString = $query['search']; + } catch (\JsonException $e) { + throw $this->createNotFoundException(); + } + + $dates = $this->uploadRepository->getAutocompletionData($queryString); + + return $this->json($dates); + } +} \ No newline at end of file diff --git a/src/Controller/Admin/Document/DeleteController.php b/src/Controller/Admin/Document/DeleteController.php new file mode 100644 index 0000000..545185a --- /dev/null +++ b/src/Controller/Admin/Document/DeleteController.php @@ -0,0 +1,36 @@ +entityManager->remove($document); + $this->entityManager->flush(); + + $this->addFlash('success', 'Das Dokument wurde gelöscht'); + $this->logger->info('Delete document', [ + 'document' => $document->getOriginalFilename(), + ]); + + return $this->redirectToRoute('app_admin_document_index'); + } +} \ No newline at end of file diff --git a/src/Controller/Admin/Document/IndexController.php b/src/Controller/Admin/Document/IndexController.php index 4b4aa18..64a586e 100644 --- a/src/Controller/Admin/Document/IndexController.php +++ b/src/Controller/Admin/Document/IndexController.php @@ -2,17 +2,40 @@ namespace App\Controller\Admin\Document; +use App\Repository\UploadRepository; +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\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; class IndexController extends AbstractController { + public function __construct( + private readonly UploadRepository $uploadRepository, + private readonly PaginatorInterface $paginator + ) { + } + #[Route('/admin/document', name: 'app_admin_document_index')] #[IsGranted('ROLE_ADMINISTRATIVE')] - public function index(): Response + public function index(Request $request): Response { - return $this->render('admin/document/index.html.twig'); + $query = $this->uploadRepository->getDocumentListQuery(); + + $pagination = $this->paginator->paginate( + $query, + $request->query->getInt('page', 1), + 10, + [ + 'defaultSortFieldName' => 'upload.originalFilename', + 'defaultSortDirection' => 'asc', + ] + ); + + return $this->render('admin/document/index.html.twig', [ + 'pagination' => $pagination, + ]); } } \ No newline at end of file diff --git a/src/Controller/Admin/Document/ReplaceController.php b/src/Controller/Admin/Document/ReplaceController.php new file mode 100644 index 0000000..a39dd87 --- /dev/null +++ b/src/Controller/Admin/Document/ReplaceController.php @@ -0,0 +1,63 @@ +uploadHandler->getUploadSession(); + if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) { + /** @var User $user */ + $user = $this->getUser(); + Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_DOCUMENT, $upload); + $this->entityManager->flush(); + $this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_DOCUMENT, $uploadSession); + $this->uploadHandler->destroyUploadSession(); + } + + $response = new AjaxModalResponseDto(); + $formAction = $this->generateUrl('app_admin_document_replace', ['uuid' => $upload->getUuid()]); + + $form = $this + ->createFormBuilder(null, ['action' => $formAction, 'ajax_submit' => true]) + ->getForm() + ; + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->addFlash('success', 'Das Dokument wurden ersetzt'); + $this->logger->info('Replace document'); + + $response->setCloseAndRedirect($this->generateUrl('app_admin_document_index')); + } else { + $response->setContent($this->renderView('admin/document/replace.html.twig', [ + 'form' => $form, + ])); + } + + return $this->json($response); + } +} \ No newline at end of file diff --git a/src/Controller/Admin/Document/UploadController.php b/src/Controller/Admin/Document/UploadController.php new file mode 100644 index 0000000..0fee9e7 --- /dev/null +++ b/src/Controller/Admin/Document/UploadController.php @@ -0,0 +1,66 @@ +uploadHandler->getUploadSession(); + if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) { + /** @var User $user */ + $user = $this->getUser(); + foreach ($uploadSession->getUploads() as $upload) { + $document = Upload::fromUploadDto($upload, $user, Upload::TYPE_DOCUMENT); + $this->entityManager->persist($document); + } + $this->entityManager->flush(); + $this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_DOCUMENT, $uploadSession); + $this->uploadHandler->destroyUploadSession(); + } + + $response = new AjaxModalResponseDto(); + $formAction = $this->generateUrl('app_admin_document_upload'); + + $form = $this + ->createFormBuilder(null, ['action' => $formAction, 'ajax_submit' => true]) + ->getForm() + ; + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->addFlash('success', 'Das/die Dokument/e wurden hochgeladen'); + $this->logger->info('Upload document'); + + $response->setCloseAndRedirect($this->generateUrl('app_admin_document_index')); + } else { + $response->setContent($this->renderView('admin/document/upload.html.twig', [ + 'form' => $form, + ])); + } + + return $this->json($response); + } +} \ No newline at end of file diff --git a/src/Entity/Assignment.php b/src/Entity/Assignment.php index 0cb0d8a..ff2b732 100644 --- a/src/Entity/Assignment.php +++ b/src/Entity/Assignment.php @@ -76,6 +76,9 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa #[ORM\ManyToMany(targetEntity: Teamer::class, mappedBy: 'bookmarks')] private Collection $teamers; + #[ORM\ManyToMany(targetEntity: Upload::class)] + private Collection $documents; + public function __construct() { $this->uuid = Uuid::v4(); @@ -84,6 +87,7 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa $this->dispositions = new ArrayCollection(); $this->teamers = new ArrayCollection(); $this->benefits = "Anreise im E&P Reisebus\nUnterkunft\nVerpflegung\nSkipass\n"; + $this->documents = new ArrayCollection(); } public function getId(): ?int @@ -344,4 +348,28 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa return $this; } + + /** + * @return Collection + */ + public function getDocuments(): Collection + { + return $this->documents; + } + + public function addDocument(Upload $document): static + { + if (!$this->documents->contains($document)) { + $this->documents->add($document); + } + + return $this; + } + + public function removeDocument(Upload $document): static + { + $this->documents->removeElement($document); + + return $this; + } } diff --git a/src/Entity/Upload.php b/src/Entity/Upload.php index bdf2336..42e5dac 100644 --- a/src/Entity/Upload.php +++ b/src/Entity/Upload.php @@ -19,6 +19,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface public const TYPE_CERTIFICATE = 'certificate'; public const TYPE_CONTRACT = 'contract'; public const TYPE_INVOICE = 'invoice'; + public const TYPE_DOCUMENT = 'document'; public const STATUS_NEW = 'new'; public const STATUS_IN_PROCESS = 'in_process'; @@ -46,6 +47,9 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface #[ORM\Column(length: 255)] private ?string $originalFilename = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $displayName = null; + #[ORM\Column] private ?int $size = null; @@ -60,9 +64,9 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface $this->uuid = Uuid::v4(); } - public static function fromUploadDto(UploadDto $uploadDto, User $owner, string $type): static + public static function fromUploadDto(UploadDto $uploadDto, User $owner, string $type, Upload $instance = null): static { - $instance = new static(); + $instance = $instance ?? new static(); $instance ->setFilename($uploadDto->getFilename()) ->setOriginalFilename($uploadDto->getOriginalFilename()) @@ -133,6 +137,18 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface return $this; } + public function getDisplayName(): ?string + { + return $this->displayName; + } + + public function setDisplayName(?string $displayName): static + { + $this->displayName = $displayName; + + return $this; + } + public function getSize(): ?int { return $this->size; diff --git a/src/Form/AssignmentType.php b/src/Form/AssignmentType.php index d459f86..ab651d4 100644 --- a/src/Form/AssignmentType.php +++ b/src/Form/AssignmentType.php @@ -6,11 +6,13 @@ use App\Entity\Assignment; use App\Entity\Destination; use App\Entity\Fee; use App\Entity\JobProfile; +use App\Entity\Upload; use App\Entity\User; use Doctrine\ORM\EntityRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; @@ -87,6 +89,19 @@ class AssignmentType extends AbstractType ; }, ]) + ->add('documents', CollectionType::class, [ + 'label' => 'Dokumente', + 'entry_type' => AutocompleteEntityType::class, + 'entry_options' => [ + 'class' => Upload::class, + 'endpoint_route' => 'app_admin_autocomplete_document', + 'label_property' => 'originalFilename', + 'placeholder' => 'Dokument...', + ], + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + ]) ->add('remarks', TextareaType::class, [ 'label' => 'Anmerkungen', 'required' => false, diff --git a/src/Repository/UploadRepository.php b/src/Repository/UploadRepository.php index 8aa6381..0472bde 100644 --- a/src/Repository/UploadRepository.php +++ b/src/Repository/UploadRepository.php @@ -3,7 +3,9 @@ namespace App\Repository; use App\Entity\Upload; +use App\Repository\Traits\QueryHelperTrait; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\Query; use Doctrine\Persistence\ManagerRegistry; /** @@ -16,51 +18,54 @@ use Doctrine\Persistence\ManagerRegistry; */ class UploadRepository extends ServiceEntityRepository { + use QueryHelperTrait; + public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Upload::class); } - public function save(Upload $entity, bool $flush = false): void + public function getDocumentListQuery(): Query { - $this->getEntityManager()->persist($entity); + $qb = $this->createQueryBuilder('upload'); - if ($flush) { - $this->getEntityManager()->flush(); - } + return $qb + ->where($qb->expr()->eq('upload.type', ':type')) + ->setParameter('type', Upload::TYPE_DOCUMENT) + ->getQuery() + ; } - public function remove(Upload $entity, bool $flush = false): void + public function getAutocompletionData(string $search): array { - $this->getEntityManager()->remove($entity); + $qb = $this->createQueryBuilder('upload'); - if ($flush) { - $this->getEntityManager()->flush(); + $documents = $qb + ->where($qb->expr()->orX( + $qb->expr()->like('upload.originalFilename', ':filename'), + $qb->expr()->like('upload.displayName', ':filename') + )) + ->setParameter('filename', '%'.$this->escapeLikeWildcards($search).'%') + ->orderBy('upload.originalFilename', 'ASC') + ->getQuery() + ->getResult() + ; + + $data = [ + [ + 'value' => '', + 'text' => '...', + ], + ]; + + foreach ($documents as $document) { + /** @var Upload $document */ + $data[] = [ + 'value' => $document->getId(), + 'text' => $document->getOriginalFilename(), + ]; } + + return $data; } - -// /** -// * @return Upload[] Returns an array of Upload objects -// */ -// public function findByExampleField($value): array -// { -// return $this->createQueryBuilder('u') -// ->andWhere('u.exampleField = :val') -// ->setParameter('val', $value) -// ->orderBy('u.id', 'ASC') -// ->setMaxResults(10) -// ->getQuery() -// ->getResult() -// ; -// } - -// public function findOneBySomeField($value): ?Upload -// { -// return $this->createQueryBuilder('u') -// ->andWhere('u.exampleField = :val') -// ->setParameter('val', $value) -// ->getQuery() -// ->getOneOrNullResult() -// ; -// } } diff --git a/templates/admin/assignment/_form.html.twig b/templates/admin/assignment/_form.html.twig index 09fb236..e929d6a 100644 --- a/templates/admin/assignment/_form.html.twig +++ b/templates/admin/assignment/_form.html.twig @@ -1,3 +1,15 @@ +{% macro collectionRow(form) %} +
+
+ {{ form_widget(form) }} + +
+ {{ form_errors(form) }} +
+{% endmacro %} + {{ form_start(form) }}
@@ -25,6 +37,22 @@
{{ form_row(form.remarks) }} +
+

+ Dokumente +

+
+ {% do form.documents.setRendered %} + {%- for document in form.documents -%}{{- _self.collectionRow(document) -}}{%- endfor -%} +
+
+ +
+
diff --git a/templates/admin/document/index.html.twig b/templates/admin/document/index.html.twig index fcf1bdb..3f45171 100644 --- a/templates/admin/document/index.html.twig +++ b/templates/admin/document/index.html.twig @@ -6,16 +6,86 @@

Dokumentenübersicht

-
-
- - +
+
+
+ + + + + + + + + + + {% for document in pagination %} + + + + + - - - -
+ {{ knp_pagination_sortable(pagination, 'Dateiname', 'upload.originalFilename') }} + + Dateigröße + + Upload am + + Upload von +
+ {{ document.originalFilename }} + + {{ document.size|file_size }} + + {{ document.createdAt|date('d.m.Y') }} + + {{ document.createdBy }} + +
+ + + + {{ icon('download') }} + +
+
-
+ {% else %} + + + Keine Daten... + + + {% endfor %} + +
+
+ {% endblock %} \ No newline at end of file diff --git a/templates/admin/document/replace.html.twig b/templates/admin/document/replace.html.twig new file mode 100644 index 0000000..194a196 --- /dev/null +++ b/templates/admin/document/replace.html.twig @@ -0,0 +1,14 @@ +{{ form_start(form) }} +
+ {% include '_partials/_upload_collection_form.html.twig' with { + 'endpoint_upload': path('_uploader_upload_document'), + 'max_filesize': 5, + 'max_files': 1, + 'accepted_files': 'application/pdf', + } %} +
+ +{{ form_rest(form) }} +{{ form_end(form) }} diff --git a/templates/admin/document/upload.html.twig b/templates/admin/document/upload.html.twig new file mode 100644 index 0000000..76a0b9f --- /dev/null +++ b/templates/admin/document/upload.html.twig @@ -0,0 +1,13 @@ +{{ form_start(form) }} +
+ {% include '_partials/_upload_collection_form.html.twig' with { + 'endpoint_upload': path('_uploader_upload_document'), + 'max_filesize': 5, + 'accepted_files': 'application/pdf', + } %} +
+ +{{ form_rest(form) }} +{{ form_end(form) }} diff --git a/templates/teamer/disposition/detail.html.twig b/templates/teamer/disposition/detail.html.twig index 3002598..9481eb1 100644 --- a/templates/teamer/disposition/detail.html.twig +++ b/templates/teamer/disposition/detail.html.twig @@ -35,7 +35,6 @@
{% include '_partials/_upload_collection_form.html.twig' with { 'endpoint_upload': path('_uploader_upload_contract'), - 'upload_session_params': null, 'accepted_files': 'image/jpg,image/jpeg,application/pdf', } %}