From 91f0f76ccc4591fd9eccea7ebd2fb8a178c20966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Sun, 22 Oct 2023 17:05:43 +0200 Subject: [PATCH] WIP: Implement admin dashboard --- assets/styles/components/badge.css | 2 +- migrations/Version20231022142623.php | 35 +++++++++ .../Admin/Assignment/CreateController.php | 5 ++ .../Admin/Document/CheckController.php | 10 +++ src/Controller/Admin/IndexController.php | 37 +++++++++- src/Entity/Assignment.php | 15 ++++ src/Entity/Upload.php | 1 + src/Form/DocumentCheckType.php | 2 + src/Repository/ApplicationRepository.php | 32 +++++++++ src/Repository/AssignmentRepository.php | 16 +++++ src/Repository/AvailabilityRepository.php | 13 ++++ src/Repository/UploadRepository.php | 41 +++++++++++ src/Security/Voter/UploadVoter.php | 2 +- templates/admin/index.html.twig | 71 +++++++++++++++++++ translations/messages.de.yaml | 2 +- 15 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 migrations/Version20231022142623.php diff --git a/assets/styles/components/badge.css b/assets/styles/components/badge.css index 18e2ad8..3e054ec 100644 --- a/assets/styles/components/badge.css +++ b/assets/styles/components/badge.css @@ -1,5 +1,5 @@ .upload-status-badge { - @apply inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium; + @apply inline-flex items-center rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap; } .upload-status-badge--default { diff --git a/migrations/Version20231022142623.php b/migrations/Version20231022142623.php new file mode 100644 index 0000000..4c474b6 --- /dev/null +++ b/migrations/Version20231022142623.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE assignment ADD owner_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE assignment ADD CONSTRAINT FK_30C544BA7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)'); + $this->addSql('CREATE INDEX IDX_30C544BA7E3C61F9 ON assignment (owner_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE assignment DROP FOREIGN KEY FK_30C544BA7E3C61F9'); + $this->addSql('DROP INDEX IDX_30C544BA7E3C61F9 ON assignment'); + $this->addSql('ALTER TABLE assignment DROP owner_id'); + } +} diff --git a/src/Controller/Admin/Assignment/CreateController.php b/src/Controller/Admin/Assignment/CreateController.php index 12cacd5..b20b9b4 100644 --- a/src/Controller/Admin/Assignment/CreateController.php +++ b/src/Controller/Admin/Assignment/CreateController.php @@ -3,6 +3,7 @@ namespace App\Controller\Admin\Assignment; use App\Entity\Assignment; +use App\Entity\User; use App\Form\AssignmentType; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -24,7 +25,11 @@ class CreateController extends AbstractController #[IsGranted('ROLE_ADMINISTRATIVE')] public function index(Request $request): Response { + /** @var User $user */ + $user = $this->getUser(); $assignment = new Assignment(); + $assignment->setOwner($user); + $form = $this->createForm(AssignmentType::class, $assignment); $form->handleRequest($request); diff --git a/src/Controller/Admin/Document/CheckController.php b/src/Controller/Admin/Document/CheckController.php index d2ad818..3e127f1 100644 --- a/src/Controller/Admin/Document/CheckController.php +++ b/src/Controller/Admin/Document/CheckController.php @@ -60,6 +60,16 @@ class CheckController extends AbstractController case Upload::STATUS_REJECTED: $this->rejectDocument($document, $formData->getComment()); $this->eventDispatcher->dispatch(new DocumentRejectedEvent($formData), DocumentRejectedEvent::NAME); + default: + $document->setStatus($formData->getStatus()); + $this->entityManager->flush(); + $this->addFlash('success', 'Der Status wurde aktualisiert'); + $this->logger->info('Update document status', [ + 'document_id' => $document->getId(), + 'document_filename' => $document->getOriginalFilename(), + 'owner' => $document->getOwner()->getFullName(), + 'status' => $formData->getStatus(), + ]); } $returnUrl = $this->generateUrl('app_admin_document_index'); diff --git a/src/Controller/Admin/IndexController.php b/src/Controller/Admin/IndexController.php index bb7a9a5..d1fa2bf 100644 --- a/src/Controller/Admin/IndexController.php +++ b/src/Controller/Admin/IndexController.php @@ -2,6 +2,12 @@ namespace App\Controller\Admin; +use App\Entity\Application; +use App\Entity\Upload; +use App\Repository\ApplicationRepository; +use App\Repository\AssignmentRepository; +use App\Repository\AvailabilityRepository; +use App\Repository\UploadRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -9,10 +15,39 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; class IndexController extends AbstractController { + public function __construct( + private readonly ApplicationRepository $applicationRepository, + private readonly AssignmentRepository $assignmentRepository, + private readonly AvailabilityRepository $availabilityRepository, + private readonly UploadRepository $uploadRepository + ) { + } + #[Route('/admin', name: 'app_admin_index')] #[IsGranted('ROLE_ADMINISTRATIVE')] public function index(): Response { - return $this->render('admin/index.html.twig'); + $applications = $this->applicationRepository->getNew(); + $newApplicationsCount = $this->applicationRepository->getCountByStatus(Application::STATUS_NEW); + $pendingApplicationsCount = $this->applicationRepository->getCountByStatus(Application::STATUS_PENDING); + + $assignments = $this->assignmentRepository->getNew(); + + $availabilities = $this->availabilityRepository->getNew(); + + $documents = $this->uploadRepository->getNew(); + $newDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_NEW); + $pendingDocumentsCount = $this->uploadRepository->getCountByStatus(Upload::STATUS_PENDING); + + return $this->render('admin/index.html.twig', [ + 'applications' => $applications, + 'newApplicationsCount' => $newApplicationsCount, + 'pendingApplicationsCount' => $pendingApplicationsCount, + 'assignments' => $assignments, + 'availabilities' => $availabilities, + 'documents' => $documents, + 'newDocumentsCount' => $newDocumentsCount, + 'pendingDocumentsCount' => $pendingDocumentsCount, + ]); } } \ No newline at end of file diff --git a/src/Entity/Assignment.php b/src/Entity/Assignment.php index 5d443c4..eb42e98 100644 --- a/src/Entity/Assignment.php +++ b/src/Entity/Assignment.php @@ -81,6 +81,9 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa #[ORM\ManyToMany(targetEntity: Upload::class)] private Collection $documents; + #[ORM\ManyToOne] + private ?User $owner = null; + public function __construct() { $this->uuid = Uuid::v4(); @@ -421,4 +424,16 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa return $this; } + + public function getOwner(): ?User + { + return $this->owner; + } + + public function setOwner(?User $owner): static + { + $this->owner = $owner; + + return $this; + } } diff --git a/src/Entity/Upload.php b/src/Entity/Upload.php index 2bd5cdf..9987b41 100644 --- a/src/Entity/Upload.php +++ b/src/Entity/Upload.php @@ -23,6 +23,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface public const TYPE_DOCUMENT = 'document'; public const STATUS_NEW = 'new'; + public const STATUS_PENDING = 'pending'; public const STATUS_CHECKED = 'checked'; public const STATUS_PAID = 'paid'; public const STATUS_REJECTED = 'rejected'; diff --git a/src/Form/DocumentCheckType.php b/src/Form/DocumentCheckType.php index 4ab305e..84c6f60 100644 --- a/src/Form/DocumentCheckType.php +++ b/src/Form/DocumentCheckType.php @@ -36,10 +36,12 @@ class DocumentCheckType extends AbstractType 'data_class' => DocumentCheckDto::class, 'status_choices' => [ Upload::TYPE_CONTRACT => [ + 'in Bearbeitung' => Upload::STATUS_PENDING, 'bestätigt' => Upload::STATUS_CHECKED, 'abgelehnt' => Upload::STATUS_REJECTED, ], Upload::TYPE_INVOICE => [ + 'in Bearbeitung' => Upload::STATUS_PENDING, 'bezahlt' => Upload::STATUS_PAID, 'abgelehnt' => Upload::STATUS_REJECTED, ], diff --git a/src/Repository/ApplicationRepository.php b/src/Repository/ApplicationRepository.php index 6c7c9af..5efb83d 100644 --- a/src/Repository/ApplicationRepository.php +++ b/src/Repository/ApplicationRepository.php @@ -72,4 +72,36 @@ class ApplicationRepository extends ServiceEntityRepository ->getResult() ; } + + public function getNew(int $limit = 5): array + { + $qb = $this->createQueryBuilder('application'); + + return $qb + ->select('application', 'assignment', 'destination', 'job_profile', 'teamer') + ->innerJoin('application.assignment', 'assignment') + ->innerJoin('assignment.destination', 'destination') + ->innerJoin('assignment.jobProfile', 'job_profile') + ->innerJoin('application.teamer', 'teamer') + ->where($qb->expr()->neq('application.status', ':status')) + ->orderBy('application.createdAt', 'DESC') + ->setParameter('status', Application::STATUS_REJECTED) + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } + + public function getCountByStatus(string $status): ?int + { + $qb = $this->createQueryBuilder('application'); + + return $qb + ->select($qb->expr()->count('application')) + ->where($qb->expr()->eq('application.status', ':status')) + ->setParameter('status', $status) + ->getQuery() + ->getSingleScalarResult() + ; + } } diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 543f407..2c7ef33 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -121,4 +121,20 @@ class AssignmentRepository extends ServiceEntityRepository return $options; } + + public function getNew(int $limit = 5): array + { + $qb = $this->createQueryBuilder('assignment'); + + return $qb + ->select('assignment', 'destination', 'job_profile', 'owner') + ->innerJoin('assignment.destination', 'destination') + ->innerJoin('assignment.jobProfile', 'job_profile') + ->innerJoin('assignment.owner', 'owner') + ->orderBy('assignment.createdAt', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/AvailabilityRepository.php b/src/Repository/AvailabilityRepository.php index d133400..3812d44 100644 --- a/src/Repository/AvailabilityRepository.php +++ b/src/Repository/AvailabilityRepository.php @@ -65,4 +65,17 @@ class AvailabilityRepository extends ServiceEntityRepository ->getResult() ; } + + public function getNew(int $limit = 5): array + { + $qb = $this->createQueryBuilder('availability'); + + return $qb + ->where($qb->expr()->isNotNull('availability.owner')) + ->orderBy('availability.createdAt', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/UploadRepository.php b/src/Repository/UploadRepository.php index eafba3c..07a08a3 100644 --- a/src/Repository/UploadRepository.php +++ b/src/Repository/UploadRepository.php @@ -6,6 +6,7 @@ use App\Entity\Upload; use App\Repository\Traits\QueryHelperTrait; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\Query; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\Persistence\ManagerRegistry; /** @@ -83,4 +84,44 @@ class UploadRepository extends ServiceEntityRepository ->getQuery() ; } + + public function getNew(int $limit = 5): array + { + $qb = $this->createQueryBuilder('upload'); + + return $qb + ->select('upload', 'owner', 'teamer', 'disposition', 'assignment') + ->innerJoin('upload.owner', 'owner') + ->innerJoin('owner.teamer', 'teamer') + ->leftJoin('upload.disposition', 'disposition') + ->leftJoin('disposition.assignment', 'assignment') + ->where($qb->expr()->andX( + $qb->expr()->in('upload.type', ':type'), + $qb->expr()->in('upload.status', ':status') + )) + ->orderBy('upload.createdAt', 'DESC') + ->setParameter('type', [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE]) + ->setParameter('status', [Upload::STATUS_NEW, Upload::STATUS_PENDING]) + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } + + public function getCountByStatus(string $status): ?int + { + $qb = $this->createQueryBuilder('upload'); + + return $qb + ->select($qb->expr()->count('upload')) + ->where($qb->expr()->andX( + $qb->expr()->in('upload.type', ':type'), + $qb->expr()->eq('upload.status', ':status') + )) + ->setParameter('type', [Upload::TYPE_CONTRACT, Upload::TYPE_INVOICE]) + ->setParameter('status', $status) + ->getQuery() + ->getSingleScalarResult() + ; + } } diff --git a/src/Security/Voter/UploadVoter.php b/src/Security/Voter/UploadVoter.php index a18afc9..f983d6f 100644 --- a/src/Security/Voter/UploadVoter.php +++ b/src/Security/Voter/UploadVoter.php @@ -32,7 +32,7 @@ class UploadVoter extends Voter if (true === in_array('ROLE_ADMINISTRATIVE', $token->getRoleNames())) { // Only new documents may be checked if (static::CHECK === $attribute) { - return Upload::STATUS_NEW === $upload->getStatus(); + return in_array($upload->getStatus(), [Upload::STATUS_NEW, Upload::STATUS_PENDING]); } return true; diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index 4415abf..b125d64 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -1,4 +1,75 @@ {% extends 'admin/layout.html.twig' %} {% block content %} +
+
+

+ Neue Bewerbungen +

+

+ {{ newApplicationsCount }} neu, {{ pendingApplicationsCount }} in Bearbeitung +

+ +
+
+

+ Neue Dokumente +

+

+ {{ newDocumentsCount }} neu {{ pendingDocumentsCount }} in Bearbeitung +

+ +
+
+

+ Neue Einsätze +

+ +
+
+

+ Neue Verfügbarkeiten +

+ +
+
+ {% endblock %} \ No newline at end of file diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml index 91fd2e4..f980119 100644 --- a/translations/messages.de.yaml +++ b/translations/messages.de.yaml @@ -18,7 +18,7 @@ label: photo: Foto status: new: neu - checking: in Bearbeitung + pending: in Bearbeitung checked: geprüft paid: bezahlt rejected: abgelehnt