diff --git a/assets/images/icons.svg b/assets/images/icons.svg index 7176be2..e43afb4 100644 --- a/assets/images/icons.svg +++ b/assets/images/icons.svg @@ -166,6 +166,9 @@ + + + diff --git a/src/Controller/Administrative/Assignment/CallOffController.php b/src/Controller/Administrative/Assignment/CallOffController.php new file mode 100644 index 0000000..a229be6 --- /dev/null +++ b/src/Controller/Administrative/Assignment/CallOffController.php @@ -0,0 +1,55 @@ +isMethod('POST')) { + $assignment->setStatus(Assignment::STATUS_CALLED_OFF); + + $this->entityManager->flush(); + + $this + ->eventDispatcher + ->dispatch(new AssignmentCalledOffEvent($assignment), AssignmentCalledOffEvent::NAME) + ; + + $this->logger->info('Call off assignment', [ + 'assignment_id' => $assignment->getId(), + 'destination' => (string) $assignment->getDestination(), + 'job_profile' => $assignment->getJobProfile()->getName(), + ]); + + $this->addFlash('success', 'Der Einsatz/die Reise wurde abgesagt'); + + return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_index')); + } + + return $this->render('administrative/assignment/modal_call_off.html.twig', [ + 'assignment' => $assignment, + ]); + } +} \ No newline at end of file diff --git a/src/Entity/Assignment.php b/src/Entity/Assignment.php index d14cca7..b72e8e5 100644 --- a/src/Entity/Assignment.php +++ b/src/Entity/Assignment.php @@ -29,6 +29,7 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa public const STATUS_STAFFING = 'staffing'; public const STATUS_UNSTAFFED = 'unstaffed'; public const STATUS_DELETED = 'deleted'; + public const STATUS_CALLED_OFF = 'called_off'; #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Event/AssignmentCalledOffEvent.php b/src/Event/AssignmentCalledOffEvent.php new file mode 100644 index 0000000..4fb922c --- /dev/null +++ b/src/Event/AssignmentCalledOffEvent.php @@ -0,0 +1,19 @@ +assignment; + } +} \ No newline at end of file diff --git a/src/EventListener/EmailNotificationSubscriber.php b/src/EventListener/EmailNotificationSubscriber.php index e2c11c4..2d26f47 100644 --- a/src/EventListener/EmailNotificationSubscriber.php +++ b/src/EventListener/EmailNotificationSubscriber.php @@ -6,6 +6,7 @@ use App\Email\Mailer; use App\Entity\Application; use App\Entity\Upload; use App\Event\ApplicationStatusEvent; +use App\Event\AssignmentCalledOffEvent; use App\Event\DispositionCreatedEvent; use App\Event\DocumentRejectedEvent; use App\Event\DocumentUploadedEvent; @@ -30,6 +31,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface DocumentUploadedEvent::NAME => 'onDocumentUploaded', DocumentRejectedEvent::NAME => 'onDocumentRejected', ApplicationStatusEvent::NAME => 'onApplicationStatus', + AssignmentCalledOffEvent::NAME => 'onAssignmentCalledOff', ]; } @@ -154,4 +156,20 @@ class EmailNotificationSubscriber implements EventSubscriberInterface 'template' => 'email/application_rejected.html.twig', ]); } + + public function onAssignmentCalledOff(AssignmentCalledOffEvent $event): void + { + $assignment = $event->getAssignment(); + foreach ($assignment->getDispositions() as $disposition) { + $teamer = $disposition->getTeamer(); + + $this->mailer->createAndSendEmail([ + 'assignment' => $assignment, + ], [ + 'to' => $teamer->getCommunication()->getEmail(), + 'subject' => 'Die Reise zu deinem Einsatz wurde abgesagt', + 'template' => 'email/assignment_called_off.html.twig', + ]); + } + } } \ No newline at end of file diff --git a/src/Form/AssignmentType.php b/src/Form/AssignmentType.php index c2324ba..f7a95fd 100644 --- a/src/Form/AssignmentType.php +++ b/src/Form/AssignmentType.php @@ -37,6 +37,7 @@ class AssignmentType extends AbstractType 'choices' => [ 'Entwurf' => Assignment::STATUS_DRAFT, 'veröffentlicht' => Assignment::STATUS_PUBLISHED, + 'abgesagt' => Assignment::STATUS_CALLED_OFF, ], ]) ->add('availableDispositions', IntegerType::class, [ diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 3ca0580..0dcea06 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -67,12 +67,14 @@ class AssignmentRepository extends ServiceEntityRepository ) ), $qb->expr()->isNull('assignment.deletedAt'), + $qb->expr()->notIn('assignment.status', ':status'), $qb->expr()->in('assignment.id', $availableAssignmentsIds) )) ->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer')) ->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer')) ->groupBy('assignment') ->setParameter('teamer', $teamer) + ->setParameter('status', [Assignment::STATUS_DRAFT, Assignment::STATUS_CALLED_OFF]) ->setParameter('now', new \DateTimeImmutable()) ; @@ -273,12 +275,12 @@ class AssignmentRepository extends ServiceEntityRepository $qb->expr()->gt('assignment.dateFrom', ':now') ) ), - $qb->expr()->neq('assignment.status', ':status'), + $qb->expr()->notIn('assignment.status', ':status'), $qb->expr()->isNull('assignment.deletedAt') )) ->groupBy('assignment') ->having($qb->expr()->gt('assignment.availableDispositions', $qb->expr()->count('disposition.id'))) - ->setParameter('status', Assignment::STATUS_DRAFT) + ->setParameter('status', [Assignment::STATUS_DRAFT, Assignment::STATUS_CALLED_OFF]) ->setParameter('now', new \DateTimeImmutable()) ->getQuery() ->getArrayResult() diff --git a/src/Repository/DispositionRepository.php b/src/Repository/DispositionRepository.php index d66e99e..fd66872 100644 --- a/src/Repository/DispositionRepository.php +++ b/src/Repository/DispositionRepository.php @@ -37,6 +37,7 @@ class DispositionRepository extends ServiceEntityRepository ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( $qb->expr()->eq('disposition.teamer', ':teamer'), + $qb->expr()->neq('assignment.status', ':status'), $qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNull('assignment.dateFrom'), @@ -49,6 +50,7 @@ class DispositionRepository extends ServiceEntityRepository ) )) ->setParameter('teamer', $teamer) + ->setParameter('status', Assignment::STATUS_CALLED_OFF) ->setParameter('date', new \DateTimeImmutable()) ->getQuery() ; diff --git a/src/Security/Voter/AssignmentVoter.php b/src/Security/Voter/AssignmentVoter.php index 95188cd..e52c322 100644 --- a/src/Security/Voter/AssignmentVoter.php +++ b/src/Security/Voter/AssignmentVoter.php @@ -17,6 +17,7 @@ class AssignmentVoter extends Voter public const EDIT = 'EDIT'; public const APPLY = 'APPLY'; public const PUBLISH = 'PUBLISH'; + public const CALL_OFF = 'CALL_OFF'; public function __construct( private readonly Security $security, @@ -30,7 +31,7 @@ class AssignmentVoter extends Voter return false; } - return in_array($attribute, [static::VIEW, static::EDIT, static::APPLY, static::PUBLISH]); + return in_array($attribute, [static::VIEW, static::EDIT, static::APPLY, static::PUBLISH, static::CALL_OFF]); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool @@ -54,8 +55,19 @@ class AssignmentVoter extends Voter && $this->security->isGranted('ROLE_ADMINISTRATIVE'); } - // Only allow applications to published assignments - if (Assignment::STATUS_DRAFT === $assignment->getStatus()) { + // Only users with administrative role may call off assignments + if (static::CALL_OFF === $attribute) { + return Assignment::STATUS_CALLED_OFF !== $assignment->getStatus() + && $this->security->isGranted('ROLE_ADMINISTRATIVE'); + } + + // Only allow applications to published, not deleted and not called off assignments + if ( + true === in_array($assignment->getStatus(), [ + Assignment::STATUS_DRAFT, + Assignment::STATUS_DELETED, + Assignment::STATUS_CALLED_OFF, + ])) { return false; } diff --git a/templates/administrative/assignment/index.html.twig b/templates/administrative/assignment/index.html.twig index 30e260a..f204ccf 100644 --- a/templates/administrative/assignment/index.html.twig +++ b/templates/administrative/assignment/index.html.twig @@ -71,26 +71,26 @@ - {% if assignment.status == 'draft' %} + {% if assignment.status == constant('App\\Entity\\Assignment::STATUS_DRAFT') %} {{ icon('edit', 'w-4 h-4') }} + {% elseif assignment.status == constant('App\\Entity\\Assignment::STATUS_CALLED_OFF') %} + {{ icon('no-symbol', 'w-4 h-4') }} + {% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFED') %} + + + + {% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_PARTLY_STAFFED') %} + + + + {% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFING') %} + + + {% else %} - {% if assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFED') %} - - - - {% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_PARTLY_STAFFED') %} - - - - {% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFING') %} - - - - {% else %} - - - - {% endif %} + + + {% endif %} @@ -167,6 +167,17 @@ hx-swap="beforeend"> löschen + {% if is_granted('CALL_OFF', assignment) %} + + absagen + + {% endif %} + + {{ icon('no-symbol', 'w-4 h-4 mr-1') }} + = abgesagt + diff --git a/templates/administrative/assignment/modal_call_off.html.twig b/templates/administrative/assignment/modal_call_off.html.twig new file mode 100644 index 0000000..e2e0f38 --- /dev/null +++ b/templates/administrative/assignment/modal_call_off.html.twig @@ -0,0 +1,8 @@ +{% extends 'htmx_confirmation_modal.html.twig' %} + +{% block content %} + + Möchtest du den Einsatz/die Reise {{ assignment.destination.product }} wirklich absagen und die + eingeteilten Teamer:innen benachrichtigen? + +{% endblock %} \ No newline at end of file diff --git a/templates/email/assignment_called_off.html.twig b/templates/email/assignment_called_off.html.twig new file mode 100644 index 0000000..2110ca8 --- /dev/null +++ b/templates/email/assignment_called_off.html.twig @@ -0,0 +1,22 @@ +{% extends 'email/layout.html.twig' %} + +{% block body %} + + Hallo aus Köln, + + + leider mussten wir die Reise zu deinem Einsatz '{{ assignment.destination }}' absagen, sodass dein Einsatz + nicht stattfinden kann. + + + Bitte entschuldige die Umstände! + + + Bei Fragen melde dich gerne unter team@ep-reisen.de + + + + Zum Portal + + +{% endblock %} \ No newline at end of file
+ leider mussten wir die Reise zu deinem Einsatz '{{ assignment.destination }}' absagen, sodass dein Einsatz + nicht stattfinden kann. +
+ Bitte entschuldige die Umstände! +
+ Bei Fragen melde dich gerne unter team@ep-reisen.de +
+ + Zum Portal + +