diff --git a/assets/images/icons.svg b/assets/images/icons.svg index 53df675..9e62d41 100644 --- a/assets/images/icons.svg +++ b/assets/images/icons.svg @@ -197,6 +197,7 @@ + diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml index d6ab107..984c6e7 100644 --- a/config/packages/oneup_uploader.yaml +++ b/config/packages/oneup_uploader.yaml @@ -30,10 +30,16 @@ oneup_uploader: namer: app.upload_namer storage: directory: '%kernel.project_dir%/uploads/document' + driver_license: + frontend: dropzone + use_orphanage: true + namer: app.upload_namer + storage: + directory: '%kernel.project_dir%/uploads/driver_license' chunks: maxage: 86400 storage: directory: '%kernel.project_dir%/uploads/temp' orphanage: maxage: 3600 - directory: '%kernel.project_dir%/uploads/orphanage' \ No newline at end of file + directory: '%kernel.project_dir%/uploads/orphanage' diff --git a/config/services.yaml b/config/services.yaml index 0fb7c9a..b7ee4eb 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -160,6 +160,13 @@ services: $datevEmailRecipient: '%env(DATEV_EMAIL_RECIPIENT)%' $datevEmailSender: '%env(DATEV_EMAIL_SENDER)%' + App\Security\RequiredCheck\RequiredTeamerCheckRegistry: + arguments: + $checks: !tagged_iterator app.required_teamer_check + + App\Security\RequiredCheck\DriverLicenseRequiredCheck: + tags: [ 'app.required_teamer_check' ] + app.upload_namer: class: App\Service\Upload\UploadNamer public: true diff --git a/migrations/Version20260227111223.php b/migrations/Version20260227111223.php new file mode 100644 index 0000000..7ca816f --- /dev/null +++ b/migrations/Version20260227111223.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE teamer ADD driver_license_upload_id INT DEFAULT NULL, ADD driver_license_declaration TINYINT(1) DEFAULT NULL, ADD driver_license_verified TINYINT(1) NOT NULL DEFAULT 0, ADD driver_license_status VARCHAR(64) NOT NULL DEFAULT \'none\', ADD driver_license_review_comment LONGTEXT DEFAULT NULL, ADD driver_license_reviewed_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', ADD driver_license_reviewed_by VARCHAR(8) DEFAULT NULL'); + $this->addSql('ALTER TABLE teamer ADD CONSTRAINT FK_15B751AE1C882DBD FOREIGN KEY (driver_license_upload_id) REFERENCES upload (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_15B751AE1C882DBD ON teamer (driver_license_upload_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE teamer DROP FOREIGN KEY FK_15B751AE1C882DBD'); + $this->addSql('DROP INDEX UNIQ_15B751AE1C882DBD ON teamer'); + $this->addSql('ALTER TABLE teamer DROP driver_license_upload_id, DROP driver_license_declaration, DROP driver_license_verified, DROP driver_license_status, DROP driver_license_review_comment, DROP driver_license_reviewed_at, DROP driver_license_reviewed_by'); + } +} diff --git a/src/Controller/Admin/IndexController.php b/src/Controller/Admin/IndexController.php index 771d189..1be426c 100644 --- a/src/Controller/Admin/IndexController.php +++ b/src/Controller/Admin/IndexController.php @@ -44,6 +44,8 @@ class IndexController extends AbstractController $newInvoices = $uploadRepository->getNewByType(Upload::TYPE_INVOICE); $newInvoicesCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_NEW, Upload::TYPE_INVOICE); $pendingInvoicesCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_PENDING, Upload::TYPE_INVOICE); + $newDriverLicenses = $uploadRepository->getNewByType(Upload::TYPE_DRIVER_LICENSE); + $newDriverLicensesCount = $uploadRepository->getCountByStatusAndType(Upload::STATUS_NEW, Upload::TYPE_DRIVER_LICENSE); $dispositionRepository = $this->entityManager->getRepository(Disposition::class); $overdueContracts = $dispositionRepository->findOverdueContracts(); @@ -64,6 +66,8 @@ class IndexController extends AbstractController 'newInvoices' => $newInvoices, 'newInvoicesCount' => $newInvoicesCount, 'pendingInvoicesCount' => $pendingInvoicesCount, + 'newDriverLicenses' => $newDriverLicenses, + 'newDriverLicensesCount' => $newDriverLicensesCount, 'overdueContracts' => $overdueContracts, 'newDispositions' => $newDispositions, 'overdueFeedbacks' => $overdueFeedbacks, diff --git a/src/Controller/Administrative/Teamer/DriverLicense/CheckController.php b/src/Controller/Administrative/Teamer/DriverLicense/CheckController.php new file mode 100644 index 0000000..a13455e --- /dev/null +++ b/src/Controller/Administrative/Teamer/DriverLicense/CheckController.php @@ -0,0 +1,107 @@ +getType()) { + throw $this->createAccessDeniedException(); + } + + $teamer = $document->getOwner()?->getTeamer(); + if (false === $teamer instanceof Teamer) { + throw $this->createNotFoundException('Teamer not found for upload'); + } + + $formData = new DriverLicenseCheckDto($document); + $form = $this->createForm(DriverLicenseCheckType::class, $formData); + $form->handleRequest($request); + + if (true === $form->isSubmitted() && true === $form->isValid()) { + $status = $formData->getStatus(); + if (null === $status) { + throw $this->createNotFoundException('Status missing'); + } + + /** @var User $user */ + $user = $this->getUser(); + + $document + ->setStatus($status) + ->setComment($formData->getComment()) + ->setApprovedAt(Upload::STATUS_CHECKED === $status ? new \DateTimeImmutable() : null) + ->setApprovedBy(Upload::STATUS_CHECKED === $status ? $user->getInitials() : null) + ; + + $teamer + ->setDriverLicenseDeclaration(true) + ->setDriverLicenseReviewComment($formData->getComment()) + ->setDriverLicenseReviewedAt(new \DateTimeImmutable()) + ->setDriverLicenseReviewedBy($user->getInitials()) + ; + + if (Upload::STATUS_CHECKED === $status) { + $teamer + ->setDriverLicenseVerified(true) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_APPROVED) + ->setDriverLicenseUpload(null) + ; + $this->entityManager->remove($document); + $this->addFlash('success', 'Der Führerschein wurde bestätigt'); + } elseif (Upload::STATUS_REJECTED === $status) { + $teamer + ->setDriverLicenseVerified(false) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_DENIED) + ; + $this->addFlash('success', 'Der Führerschein wurde abgelehnt'); + } else { + $teamer + ->setDriverLicenseVerified(false) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_PENDING_REVIEW) + ; + $this->addFlash('success', 'Der Führerscheinstatus wurde aktualisiert'); + } + + $this->entityManager->flush(); + + $this->logger->info('Update driver license check', [ + 'teamer_id' => $teamer->getId(), + 'teamer_name' => (string) $teamer, + 'status' => $status, + ]); + + return new HxRedirectResponse($this->generateUrl('app_admin_index')); + } + + return $this->render('administrative/teamer/driver_license/modal_check.html.twig', [ + 'document' => $document, + 'teamer' => $teamer, + 'form' => $form->createView(), + ]); + } +} diff --git a/src/Controller/Teamer/Check/DriverLicenseController.php b/src/Controller/Teamer/Check/DriverLicenseController.php new file mode 100644 index 0000000..61742c2 --- /dev/null +++ b/src/Controller/Teamer/Check/DriverLicenseController.php @@ -0,0 +1,147 @@ +getUser(); + $teamer = $user->getTeamer(); + + if (null === $teamer) { + $teamer = new Teamer(); + $user->setTeamer($teamer); + $this->entityManager->persist($teamer); + $this->entityManager->flush(); + } + + $uploadSession = $this->uploadHandler->getUploadSession(); + if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) { + $this->updateDriverLicenseUpload($user, $teamer, $uploadSession); + } + + $formData = new DriverLicenseDeclarationDto($teamer->getDriverLicenseDeclaration()); + $form = $this->createForm(DriverLicenseDeclarationType::class, $formData, ['upload_session' => $uploadSession]); + $form->handleRequest($request); + + if (true === $form->isSubmitted() && true === $form->isValid()) { + if (false === $formData->hasDriverLicense()) { + $this->handleNoDriverLicense($teamer); + $this->entityManager->flush(); + + return $this->redirectToRoute('app_teamer_index'); + } + + if (0 < $form->getErrors(true)->count()) { + // Continue rendering form with errors. + } elseif (null === $teamer->getDriverLicenseUpload()) { + $form->addError(new FormError('Bitte lade ein Foto oder PDF deines Führerscheins hoch.')); + } elseif ( + Teamer::DRIVER_LICENSE_STATUS_DENIED === $teamer->getDriverLicenseStatus() + && Upload::STATUS_REJECTED === $teamer->getDriverLicenseUpload()->getStatus() + ) { + $form->addError(new FormError('Bitte lade einen neuen Führerschein hoch, damit wir ihn erneut prüfen können.')); + } else { + $teamer + ->setDriverLicenseDeclaration(true) + ->setDriverLicenseVerified(false) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_PENDING_REVIEW) + ->setDriverLicenseReviewComment(null) + ->setDriverLicenseReviewedAt(null) + ->setDriverLicenseReviewedBy(null) + ; + + $upload = $teamer->getDriverLicenseUpload(); + $upload + ->setStatus(Upload::STATUS_NEW) + ->setComment(null) + ->setApprovedAt(null) + ->setApprovedBy(null) + ; + + $this->entityManager->flush(); + + $this->addFlash('success', 'Dein Führerschein wurde hochgeladen und wird geprüft.'); + $this->logger->info('Submit driver license for review', [ + 'teamer_id' => $teamer->getId(), + 'teamer_name' => (string) $teamer, + ]); + + return $this->redirectToRoute('app_teamer_index'); + } + } + + return $this->render('teamer/check/driver_license.html.twig', [ + 'form' => $form->createView(), + 'teamer' => $teamer, + ]); + } + + private function handleNoDriverLicense(Teamer $teamer): void + { + if (null !== $existingUpload = $teamer->getDriverLicenseUpload()) { + $this->entityManager->remove($existingUpload); + } + + $teamer + ->setDriverLicenseUpload(null) + ->setDriverLicenseDeclaration(false) + ->setDriverLicenseVerified(false) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_NONE) + ->setDriverLicenseReviewComment(null) + ->setDriverLicenseReviewedAt(null) + ->setDriverLicenseReviewedBy(null) + ; + + $this->addFlash('success', 'Deine Angabe wurde gespeichert.'); + $this->logger->info('Set no driver license', [ + 'teamer_id' => $teamer->getId(), + 'teamer_name' => (string) $teamer, + ]); + } + + private function updateDriverLicenseUpload(User $user, Teamer $teamer, UploadSessionDto $uploadSession): void + { + if (null !== $existingUpload = $teamer->getDriverLicenseUpload()) { + $this->entityManager->remove($existingUpload); + } + + $upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_DRIVER_LICENSE); + $upload->setStatus(Upload::STATUS_NEW); + + $teamer->setDriverLicenseUpload($upload); + $this->entityManager->flush(); + + $this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_DRIVER_LICENSE, $uploadSession); + $this->uploadHandler->destroyUploadSession(); + + $this->addFlash('success', 'Der Führerschein wurde hochgeladen.'); + } +} diff --git a/src/Entity/Teamer.php b/src/Entity/Teamer.php index b2ea2de..e2a68ce 100644 --- a/src/Entity/Teamer.php +++ b/src/Entity/Teamer.php @@ -23,6 +23,11 @@ class Teamer implements TimestampableEntityInterface public const STATUS_NEW = 'new'; public const STATUS_EXISTING = 'existing'; + public const DRIVER_LICENSE_STATUS_NONE = 'none'; + public const DRIVER_LICENSE_STATUS_PENDING_REVIEW = 'pending_review'; + public const DRIVER_LICENSE_STATUS_APPROVED = 'approved'; + public const DRIVER_LICENSE_STATUS_DENIED = 'denied'; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -140,6 +145,27 @@ class Teamer implements TimestampableEntityInterface #[ORM\Column] private bool $allowOverlappingApplications = false; + #[ORM\Column(nullable: true)] + private ?bool $driverLicenseDeclaration = null; + + #[ORM\Column] + private bool $driverLicenseVerified = false; + + #[ORM\Column(length: 64)] + private string $driverLicenseStatus = self::DRIVER_LICENSE_STATUS_NONE; + + #[ORM\OneToOne(cascade: ['persist', 'remove'])] + private ?Upload $driverLicenseUpload = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $driverLicenseReviewComment = null; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + private ?\DateTimeImmutable $driverLicenseReviewedAt = null; + + #[ORM\Column(length: 8, nullable: true)] + private ?string $driverLicenseReviewedBy = null; + public function __construct() { $this->uuid = Uuid::v4(); @@ -750,7 +776,91 @@ class Teamer implements TimestampableEntityInterface public function hasPickup(int $pickupId): bool { - return in_array($pickupId, $this->getPickups()); + return true === in_array($pickupId, $this->getPickups(), true); + } + + public function getDriverLicenseDeclaration(): ?bool + { + return $this->driverLicenseDeclaration; + } + + public function setDriverLicenseDeclaration(?bool $driverLicenseDeclaration): static + { + $this->driverLicenseDeclaration = $driverLicenseDeclaration; + + return $this; + } + + public function isDriverLicenseVerified(): bool + { + return $this->driverLicenseVerified; + } + + public function setDriverLicenseVerified(bool $driverLicenseVerified): static + { + $this->driverLicenseVerified = $driverLicenseVerified; + + return $this; + } + + public function getDriverLicenseStatus(): string + { + return $this->driverLicenseStatus; + } + + public function setDriverLicenseStatus(string $driverLicenseStatus): static + { + $this->driverLicenseStatus = $driverLicenseStatus; + + return $this; + } + + public function getDriverLicenseUpload(): ?Upload + { + return $this->driverLicenseUpload; + } + + public function setDriverLicenseUpload(?Upload $driverLicenseUpload): static + { + $this->driverLicenseUpload = $driverLicenseUpload; + + return $this; + } + + public function getDriverLicenseReviewComment(): ?string + { + return $this->driverLicenseReviewComment; + } + + public function setDriverLicenseReviewComment(?string $driverLicenseReviewComment): static + { + $this->driverLicenseReviewComment = $driverLicenseReviewComment; + + return $this; + } + + public function getDriverLicenseReviewedAt(): ?\DateTimeImmutable + { + return $this->driverLicenseReviewedAt; + } + + public function setDriverLicenseReviewedAt(?\DateTimeImmutable $driverLicenseReviewedAt): static + { + $this->driverLicenseReviewedAt = $driverLicenseReviewedAt; + + return $this; + } + + public function getDriverLicenseReviewedBy(): ?string + { + return $this->driverLicenseReviewedBy; + } + + public function setDriverLicenseReviewedBy(?string $driverLicenseReviewedBy): static + { + $this->driverLicenseReviewedBy = $driverLicenseReviewedBy; + + return $this; } public function getLanguage(): ?string diff --git a/src/Entity/Upload.php b/src/Entity/Upload.php index d0db200..f98761b 100644 --- a/src/Entity/Upload.php +++ b/src/Entity/Upload.php @@ -21,6 +21,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface public const TYPE_CONTRACT = 'contract'; public const TYPE_INVOICE = 'invoice'; public const TYPE_DOCUMENT = 'document'; + public const TYPE_DRIVER_LICENSE = 'driver_license'; public const STATUS_NEW = 'new'; public const STATUS_PENDING = 'pending'; @@ -133,6 +134,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface self::TYPE_CONTRACT => 'Honorarvertrag', self::TYPE_INVOICE => 'Honorarnote', self::TYPE_DOCUMENT => 'Info', + self::TYPE_DRIVER_LICENSE => 'Führerschein', default => 'Dokument', }; } diff --git a/src/EventListener/RequiredTeamerCheckSubscriber.php b/src/EventListener/RequiredTeamerCheckSubscriber.php new file mode 100644 index 0000000..9d594d4 --- /dev/null +++ b/src/EventListener/RequiredTeamerCheckSubscriber.php @@ -0,0 +1,65 @@ + ['onKernelRequest', 5], + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + if (false === $event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + $route = (string) $request->attributes->get('_route'); + + if ('' === $route || false === str_starts_with($route, 'app_teamer_')) { + return; + } + + $user = $this->security->getUser(); + + if (false === $user instanceof User || false === $this->security->isGranted('ROLE_TEAMER')) { + return; + } + + $check = $this->requiredTeamerCheckRegistry->getFirstUnresolvedCheck($user); + + if (null === $check) { + return; + } + + if (true === in_array($route, [ + $check->getRouteName(), + 'app_upload_delete', + 'app_security_logout', + ], true)) { + return; + } + + $event->setResponse(new RedirectResponse($this->urlGenerator->generate($check->getRouteName()))); + } +} diff --git a/src/Form/DriverLicenseCheckType.php b/src/Form/DriverLicenseCheckType.php new file mode 100644 index 0000000..d5a92ae --- /dev/null +++ b/src/Form/DriverLicenseCheckType.php @@ -0,0 +1,43 @@ +add('status', ChoiceType::class, [ + 'label' => 'neuer Status', + 'choices' => [ + 'in Bearbeitung' => Upload::STATUS_NEW, + 'bestätigt' => Upload::STATUS_CHECKED, + 'abgelehnt' => Upload::STATUS_REJECTED, + ], + ]) + ->add('comment', TextareaType::class, [ + 'label' => 'Kommentar/Begründung', + 'required' => false, + 'attr' => [ + 'rows' => 3, + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => DriverLicenseCheckDto::class, + 'anti_xss' => true, + ]); + } +} diff --git a/src/Form/DriverLicenseDeclarationType.php b/src/Form/DriverLicenseDeclarationType.php new file mode 100644 index 0000000..44b8fa6 --- /dev/null +++ b/src/Form/DriverLicenseDeclarationType.php @@ -0,0 +1,49 @@ +add('hasDriverLicense', ChoiceType::class, [ + 'label' => 'Besitzt du einen gültigen Führerschein?', + 'choices' => [ + 'Ja' => true, + 'Nein' => false, + ], + 'expanded' => true, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options): void + { + /** @var UploadSessionDto $uploadSession */ + $uploadSession = $options['upload_session']; + + $view->vars['upload_session_params'] = [ + UploadHandler::SESSION_KEY => $uploadSession->getUid(), + ]; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'data_class' => DriverLicenseDeclarationDto::class, + ]) + ->setRequired(['upload_session']) + ->setAllowedTypes('upload_session', UploadSessionDto::class) + ; + } +} diff --git a/src/Form/TeamerFilterType.php b/src/Form/TeamerFilterType.php index 6ae506a..3844db9 100644 --- a/src/Form/TeamerFilterType.php +++ b/src/Form/TeamerFilterType.php @@ -36,6 +36,10 @@ class TeamerFilterType extends AbstractType 'label' => 'Snowboardlehrer:innen-Lizenz', 'required' => false, ]) + ->add('driverLicenseVerified', CheckboxType::class, [ + 'label' => 'Führerschein bestätigt', + 'required' => false, + ]) ->add('jobProfiles', MultiselectEntityType::class, [ 'label' => 'Job-Profile', 'class' => JobProfile::class, diff --git a/src/Model/DriverLicenseCheckDto.php b/src/Model/DriverLicenseCheckDto.php new file mode 100644 index 0000000..47c2a30 --- /dev/null +++ b/src/Model/DriverLicenseCheckDto.php @@ -0,0 +1,48 @@ +upload = $upload; + $this->status = $upload->getStatus(); + $this->comment = $upload->getComment(); + } + + public function getUpload(): Upload + { + return $this->upload; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): static + { + $this->status = $status; + + return $this; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function setComment(?string $comment): static + { + $this->comment = $comment; + + return $this; + } +} diff --git a/src/Model/DriverLicenseDeclarationDto.php b/src/Model/DriverLicenseDeclarationDto.php new file mode 100644 index 0000000..b020da4 --- /dev/null +++ b/src/Model/DriverLicenseDeclarationDto.php @@ -0,0 +1,28 @@ +hasDriverLicense = $hasDriverLicense; + } + + public function hasDriverLicense(): ?bool + { + return $this->hasDriverLicense; + } + + public function setHasDriverLicense(?bool $hasDriverLicense): static + { + $this->hasDriverLicense = $hasDriverLicense; + + return $this; + } +} diff --git a/src/Model/TeamerFilterDto.php b/src/Model/TeamerFilterDto.php index 16ccf33..3368c32 100644 --- a/src/Model/TeamerFilterDto.php +++ b/src/Model/TeamerFilterDto.php @@ -10,6 +10,7 @@ class TeamerFilterDto extends AbstractFilterDto protected string $pickupId = ''; protected bool $skiLicense = false; protected bool $snowboardLicense = false; + protected bool $driverLicenseVerified = false; protected bool $noTrainings = false; public function getName(): ?string @@ -89,6 +90,18 @@ class TeamerFilterDto extends AbstractFilterDto return $this->noTrainings; } + public function hasDriverLicenseVerified(): bool + { + return $this->driverLicenseVerified; + } + + public function setDriverLicenseVerified(bool $driverLicenseVerified): static + { + $this->driverLicenseVerified = $driverLicenseVerified; + + return $this; + } + public function setNoTrainings(bool $noTrainings): void { $this->noTrainings = $noTrainings; diff --git a/src/Repository/TeamerRepository.php b/src/Repository/TeamerRepository.php index 7600a53..366dd85 100644 --- a/src/Repository/TeamerRepository.php +++ b/src/Repository/TeamerRepository.php @@ -97,6 +97,13 @@ class TeamerRepository extends ServiceEntityRepository ; } + if (true === $filterDto->hasDriverLicenseVerified()) { + $qb + ->andWhere($qb->expr()->eq('teamer.driverLicenseVerified', ':driverLicenseVerified')) + ->setParameter('driverLicenseVerified', true) + ; + } + return $qb->getQuery(); } diff --git a/src/Security/BpnAuthenticator.php b/src/Security/BpnAuthenticator.php index 3bfd09d..ef36e0b 100644 --- a/src/Security/BpnAuthenticator.php +++ b/src/Security/BpnAuthenticator.php @@ -8,6 +8,7 @@ use App\BusProNet\Model\CrmAttributesResponse; use App\BusProNet\Model\ProfileResponse; use App\BusProNet\UserDataHandler; use App\Entity\User; +use App\Security\RequiredCheck\RequiredTeamerCheckRegistry; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -31,6 +32,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent private readonly ApiClient $apiClient, private readonly LoggerInterface $logger, private readonly UserDataHandler $userDataHandler, + private readonly RequiredTeamerCheckRegistry $requiredTeamerCheckRegistry, ) { } @@ -91,7 +93,15 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent $this->entityManager->flush(); - $url = $this->urlGenerator->generate($user->getDefaultRoute()); + $route = $user->getDefaultRoute(); + if ('app_teamer_index' === $route) { + $check = $this->requiredTeamerCheckRegistry->getFirstUnresolvedCheck($user); + if (null !== $check) { + $route = $check->getRouteName(); + } + } + + $url = $this->urlGenerator->generate($route); return new RedirectResponse($url); } diff --git a/src/Security/RequiredCheck/DriverLicenseRequiredCheck.php b/src/Security/RequiredCheck/DriverLicenseRequiredCheck.php new file mode 100644 index 0000000..4d56b0d --- /dev/null +++ b/src/Security/RequiredCheck/DriverLicenseRequiredCheck.php @@ -0,0 +1,72 @@ +hasRole('ROLE_TEAMER'); + } + + public function isSatisfied(User $user): bool + { + $teamer = $user->getTeamer(); + + if (null === $teamer) { + return false; + } + + $declaration = $teamer->getDriverLicenseDeclaration(); + + if (null === $declaration) { + return false; + } + + if (false === $declaration) { + return true; + } + + if (true === $teamer->isDriverLicenseVerified()) { + return true; + } + + if (Teamer::DRIVER_LICENSE_STATUS_APPROVED === $teamer->getDriverLicenseStatus()) { + return true; + } + + $upload = $teamer->getDriverLicenseUpload(); + + if (null === $upload) { + return false; + } + + if (Upload::TYPE_DRIVER_LICENSE !== $upload->getType()) { + return false; + } + + if (Teamer::DRIVER_LICENSE_STATUS_DENIED === $teamer->getDriverLicenseStatus()) { + return false; + } + + if (Upload::STATUS_REJECTED === $upload->getStatus()) { + return false; + } + + return true; + } + + public function getRouteName(): string + { + return 'app_teamer_check_driver_license'; + } +} diff --git a/src/Security/RequiredCheck/RequiredTeamerCheckInterface.php b/src/Security/RequiredCheck/RequiredTeamerCheckInterface.php new file mode 100644 index 0000000..a545fe4 --- /dev/null +++ b/src/Security/RequiredCheck/RequiredTeamerCheckInterface.php @@ -0,0 +1,16 @@ + $checks + */ + public function __construct(private readonly iterable $checks) + { + } + + public function getFirstUnresolvedCheck(User $user): ?RequiredTeamerCheckInterface + { + foreach ($this->checks as $check) { + if (false === $check->appliesTo($user)) { + continue; + } + + if (false === $check->isSatisfied($user)) { + return $check; + } + } + + return null; + } +} diff --git a/src/Service/Common/TeamerFilterHandler.php b/src/Service/Common/TeamerFilterHandler.php index 39382f8..4c09f36 100644 --- a/src/Service/Common/TeamerFilterHandler.php +++ b/src/Service/Common/TeamerFilterHandler.php @@ -25,6 +25,7 @@ class TeamerFilterHandler extends AbstractFilterHandler ->setPickupId($data['pickup_id']) ->setSkiLicense($data['ski_license']) ->setSnowboardLicense($data['snowboard_license']) + ->setDriverLicenseVerified($data['driver_license_verified']) ->setNoTrainings($data['no_trainings']) ; @@ -49,6 +50,7 @@ class TeamerFilterHandler extends AbstractFilterHandler 'pickup_id' => $filterDto->getPickupId(), 'ski_license' => $filterDto->hasSkiLicense(), 'snowboard_license' => $filterDto->hasSnowboardLicense(), + 'driver_license_verified' => $filterDto->hasDriverLicenseVerified(), 'no_trainings' => $filterDto->hasNoTrainings(), 'job_profiles' => array_map(function (JobProfile $jobProfile) { return $jobProfile->getId(); diff --git a/templates/_partials/_teamer_license_info.html.twig b/templates/_partials/_teamer_license_info.html.twig index 25d19e3..dbb3372 100644 --- a/templates/_partials/_teamer_license_info.html.twig +++ b/templates/_partials/_teamer_license_info.html.twig @@ -2,6 +2,11 @@ Lizenzen diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig index 299896a..eb3c4cc 100644 --- a/templates/admin/index.html.twig +++ b/templates/admin/index.html.twig @@ -121,6 +121,37 @@ +
+

+ Neue Führerscheine + {% if newDriverLicensesCount > 0 %} + {{ newDriverLicensesCount }} neu + {% endif %} +

+
+
    + {% for document in newDriverLicenses %} +
  • + +
  • + {% else %} +
  • + - +
  • + {% endfor %} +
+
+
+

Neue Einsätze diff --git a/templates/administrative/teamer/driver_license/modal_check.html.twig b/templates/administrative/teamer/driver_license/modal_check.html.twig new file mode 100644 index 0000000..56388cd --- /dev/null +++ b/templates/administrative/teamer/driver_license/modal_check.html.twig @@ -0,0 +1,25 @@ +{% extends 'htmx_modal.html.twig' %} + +{% block title %}Führerschein prüfen{% endblock %} + +{% block content %} + {{ form_start(form, { 'attr': { 'hx-post': app.request.uri, 'hx-target': '#htmx-modal', 'hx-swap': 'outerHTML' } }) }} + +

+ Führerschein {{ teamer }} vom {{ document.createdAt|date('d.m.Y') }} +

+ {{ icon('eye') }} +
+
+ {{ form_row(form.status) }} + {{ form_row(form.comment) }} +
+ + {{ form_rest(form) }} + {{ form_end(form) }} +{% endblock %} diff --git a/templates/administrative/teamer/index.html.twig b/templates/administrative/teamer/index.html.twig index 03416a3..54545e7 100644 --- a/templates/administrative/teamer/index.html.twig +++ b/templates/administrative/teamer/index.html.twig @@ -94,6 +94,9 @@
+ {% if teamer.driverLicenseVerified %} + {{ icon('car', 'w-5 h-5 text-green-500') }} + {% endif %} {% if not teamer.viewed %} +
+ + {{ form_rest(form) }} + {{ form_end(form) }} +{% endblock %} diff --git a/templates/teamer/profile/index.html.twig b/templates/teamer/profile/index.html.twig index 075b20f..f3271dd 100644 --- a/templates/teamer/profile/index.html.twig +++ b/templates/teamer/profile/index.html.twig @@ -127,6 +127,27 @@ {{ form_row(form.healthInsuranceCompany) }} {{ form_row(form.language) }} {{ form_row(form.size) }} +
+
Führerschein
+
+ {% if teamer.driverLicenseDeclaration is same as(null) %} + noch nicht angegeben + {% elseif teamer.driverLicenseDeclaration %} + {% if teamer.driverLicenseVerified %} + bestätigt + {% elseif teamer.driverLicenseStatus == constant('App\\Entity\\Teamer::DRIVER_LICENSE_STATUS_DENIED') %} + abgelehnt + {% else %} + in Prüfung + {% endif %} + {% else %} + nicht vorhanden + {% endif %} +
+ + Führerscheinstatus bearbeiten + +

Buszustiege diff --git a/tests/Security/RequiredCheck/DriverLicenseCheckDtoTest.php b/tests/Security/RequiredCheck/DriverLicenseCheckDtoTest.php new file mode 100644 index 0000000..cdafef6 --- /dev/null +++ b/tests/Security/RequiredCheck/DriverLicenseCheckDtoTest.php @@ -0,0 +1,24 @@ +setType(Upload::TYPE_DRIVER_LICENSE) + ->setStatus(Upload::STATUS_NEW) + ; + + $dto = new DriverLicenseCheckDto($upload); + + $this->assertSame(Upload::STATUS_NEW, $dto->getStatus()); + } +} diff --git a/tests/Security/RequiredCheck/DriverLicenseRequiredCheckTest.php b/tests/Security/RequiredCheck/DriverLicenseRequiredCheckTest.php new file mode 100644 index 0000000..74ed1d3 --- /dev/null +++ b/tests/Security/RequiredCheck/DriverLicenseRequiredCheckTest.php @@ -0,0 +1,108 @@ +check = new DriverLicenseRequiredCheck(); + } + + + public function testAppliesToMixedRoleUsersWithTeamerRole(): void + { + $user = $this->createTeamerUser(); + $user->setRoles(['ROLE_ADMIN', 'ROLE_TEAMER']); + + $this->assertTrue($this->check->appliesTo($user)); + } + + public function testIsNotSatisfiedWhenDeclarationIsMissing(): void + { + $user = $this->createTeamerUser(); + + $this->assertFalse($this->check->isSatisfied($user)); + } + + public function testIsSatisfiedWhenTeamerHasNoDriverLicense(): void + { + $user = $this->createTeamerUser(); + $user->getTeamer()?->setDriverLicenseDeclaration(false); + + $this->assertTrue($this->check->isSatisfied($user)); + } + + public function testIsNotSatisfiedWhenRejected(): void + { + $user = $this->createTeamerUser(); + $teamer = $user->getTeamer(); + + $upload = (new Upload()) + ->setType(Upload::TYPE_DRIVER_LICENSE) + ->setStatus(Upload::STATUS_REJECTED) + ; + + $teamer + ?->setDriverLicenseDeclaration(true) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_DENIED) + ->setDriverLicenseUpload($upload) + ; + + $this->assertFalse($this->check->isSatisfied($user)); + } + + public function testIsSatisfiedWhenSubmittedForReview(): void + { + $user = $this->createTeamerUser(); + $teamer = $user->getTeamer(); + + $upload = (new Upload()) + ->setType(Upload::TYPE_DRIVER_LICENSE) + ->setStatus(Upload::STATUS_NEW) + ; + + $teamer + ?->setDriverLicenseDeclaration(true) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_PENDING_REVIEW) + ->setDriverLicenseUpload($upload) + ; + + $this->assertTrue($this->check->isSatisfied($user)); + } + + public function testIsSatisfiedWhenApprovedWithoutUpload(): void + { + $user = $this->createTeamerUser(); + $teamer = $user->getTeamer(); + + $teamer + ?->setDriverLicenseDeclaration(true) + ->setDriverLicenseVerified(true) + ->setDriverLicenseStatus(Teamer::DRIVER_LICENSE_STATUS_APPROVED) + ->setDriverLicenseUpload(null) + ; + + $this->assertTrue($this->check->isSatisfied($user)); + } + + private function createTeamerUser(): User + { + $teamer = new Teamer(); + + return (new User()) + ->setRoles(['ROLE_TEAMER']) + ->setTeamer($teamer) + ; + } +} diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml index 1a4a023..27ada81 100644 --- a/translations/messages.de.yaml +++ b/translations/messages.de.yaml @@ -19,6 +19,7 @@ label: contract: Honorarvertrag certificate: Lizenznachweis photo: Foto + driver_license: Führerschein status: new: neu pending: in Bearbeitung