feat: call off single disposition as admin with optional notification
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\Disposition;
|
||||
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
use App\Event\DispositionCalledOffEvent;
|
||||
use App\Form\DispositionCallOffType;
|
||||
use App\Htmx\HxRedirectResponse;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class CallOffController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/administrative/disposition/call-off/{uuid}', name: 'app_administrative_disposition_call_off')]
|
||||
#[IsGranted('CALL_OFF', subject: 'disposition')]
|
||||
public function index(Disposition $disposition, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(DispositionCallOffType::class, $disposition, ['hx_post' => $request->getUri()]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$disposition->setStatus(Assignment::STATUS_CALLED_OFF);
|
||||
$sendNotification = $form->get('sendNotification')->getData();
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this
|
||||
->eventDispatcher
|
||||
->dispatch(new DispositionCalledOffEvent($disposition, $sendNotification), DispositionCalledOffEvent::NAME)
|
||||
;
|
||||
|
||||
$assignment = $disposition->getAssignment();
|
||||
|
||||
$this->logger->info('Call off disposition', [
|
||||
'assignment_id' => $disposition->getId(),
|
||||
'destination' => (string) $assignment->getDestination(),
|
||||
'job_profile' => $assignment->getJobProfile()->getName(),
|
||||
]);
|
||||
|
||||
$this->addFlash('success', 'Der Einsatz wurde abgesagt');
|
||||
|
||||
return new HxRedirectResponse($this->generateUrl('app_administrative_assignment_detail', [
|
||||
'uuid' => $assignment->getUuid(),
|
||||
]));
|
||||
}
|
||||
|
||||
return $this->render('administrative/disposition/modal_call_off.html.twig', [
|
||||
'disposition' => $disposition,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -487,6 +487,13 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
});
|
||||
}
|
||||
|
||||
public function getActiveDispositions(): Collection
|
||||
{
|
||||
return $this->dispositions->filter(function (Disposition $disposition) {
|
||||
return Disposition::STATUS_CALLED_OFF !== $disposition->getStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public function getConfirmedDispositions(): Collection
|
||||
{
|
||||
return $this->dispositions->filter(function (Disposition $disposition) {
|
||||
|
||||
@@ -25,6 +25,10 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
public const STATUS_CHECKING_INVOICE = 'checking_invoice';
|
||||
public const STATUS_PAID = 'paid';
|
||||
public const STATUS_COMPLETED = 'completed';
|
||||
public const STATUS_CALLED_OFF = 'called_off';
|
||||
|
||||
public const CALLED_OFF_BY_TEAMER = 'teamer';
|
||||
public const CALLED_OFF_BY_OFFICE = 'office';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -59,6 +63,12 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
#[ORM\JoinColumn(onDelete: 'SET NULL')]
|
||||
private ?Feedback $feedback = null;
|
||||
|
||||
#[ORM\Column(length: 64, nullable: true)]
|
||||
private ?string $calledOffBy = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $calledOffReason = null;
|
||||
|
||||
public function __construct(Application $application)
|
||||
{
|
||||
$this->uuid = Uuid::v4();
|
||||
@@ -243,4 +253,28 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
|
||||
return $period->isStarted();
|
||||
}
|
||||
|
||||
public function getCalledOffBy(): ?string
|
||||
{
|
||||
return $this->calledOffBy;
|
||||
}
|
||||
|
||||
public function setCalledOffBy(?string $calledOffBy): static
|
||||
{
|
||||
$this->calledOffBy = $calledOffBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCalledOffReason(): ?string
|
||||
{
|
||||
return $this->calledOffReason;
|
||||
}
|
||||
|
||||
public function setCalledOffReason(?string $calledOffReason): static
|
||||
{
|
||||
$this->calledOffReason = $calledOffReason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Disposition;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class DispositionCalledOffEvent extends Event
|
||||
{
|
||||
public const NAME = 'disposition.called_off';
|
||||
|
||||
public function __construct(private readonly Disposition $disposition, private readonly bool $sendNotification = false)
|
||||
{
|
||||
}
|
||||
|
||||
public function getDisposition(): Disposition
|
||||
{
|
||||
return $this->disposition;
|
||||
}
|
||||
|
||||
public function isSendNotification(): bool
|
||||
{
|
||||
return $this->sendNotification;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Entity\Application;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\ApplicationStatusEvent;
|
||||
use App\Event\AssignmentCalledOffEvent;
|
||||
use App\Event\DispositionCalledOffEvent;
|
||||
use App\Event\DispositionCreatedEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\Event\DocumentRejectedEvent;
|
||||
@@ -21,7 +22,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
public function __construct(
|
||||
private readonly Mailer $mailer,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly ContractRenderer $contractRenderer
|
||||
private readonly ContractRenderer $contractRenderer,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
return [
|
||||
DispositionCreatedEvent::NAME => 'onDispositionCreated',
|
||||
DispositionCalledOffEvent::NAME => 'onDispositionCalledOff',
|
||||
DocumentUploadedEvent::NAME => 'onDocumentUploaded',
|
||||
DocumentRejectedEvent::NAME => 'onDocumentRejected',
|
||||
ApplicationStatusEvent::NAME => 'onApplicationStatus',
|
||||
@@ -38,7 +40,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about accepted application
|
||||
* Notify teamer about accepted application.
|
||||
*/
|
||||
public function onDispositionCreated(DispositionCreatedEvent $event): void
|
||||
{
|
||||
@@ -97,7 +99,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify managers about uploaded documents
|
||||
* Notify managers about uploaded documents.
|
||||
*/
|
||||
public function onDocumentUploaded(DocumentUploadedEvent $event): void
|
||||
{
|
||||
@@ -142,7 +144,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about rejected document
|
||||
* Notify teamer about rejected document.
|
||||
*/
|
||||
public function onDocumentRejected(DocumentRejectedEvent $event): void
|
||||
{
|
||||
@@ -168,7 +170,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about rejected application
|
||||
* Notify teamer about rejected application.
|
||||
*/
|
||||
public function onApplicationStatus(ApplicationStatusEvent $event): void
|
||||
{
|
||||
@@ -189,6 +191,31 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify teamer about called-off disposition (if applicable).
|
||||
*/
|
||||
public function onDispositionCalledOff(DispositionCalledOffEvent $event): void
|
||||
{
|
||||
if (false === $event->isSendNotification()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$disposition = $event->getDisposition();
|
||||
$teamer = $disposition->getTeamer();
|
||||
|
||||
$this->mailer->createAndSendEmail([
|
||||
'assignment' => $disposition->getAssignment(),
|
||||
'reason' => $disposition->getCalledOffReason(),
|
||||
], [
|
||||
'to' => $teamer->getCommunication()->getEmail(),
|
||||
'subject' => 'Dein Einsatz wurde abgesagt',
|
||||
'template' => 'email/disposition_called_off.html.twig',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all assigned teamers about called-off assignment.
|
||||
*/
|
||||
public function onAssignmentCalledOff(AssignmentCalledOffEvent $event): void
|
||||
{
|
||||
$assignment = $event->getAssignment();
|
||||
@@ -204,4 +231,4 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Entity\Upload;
|
||||
use App\Event\ApplicationCreatedEvent;
|
||||
use App\Event\ApplicationDeletedEvent;
|
||||
use App\Event\ApplicationStatusEvent;
|
||||
use App\Event\DispositionCalledOffEvent;
|
||||
use App\Event\DispositionCreatedEvent;
|
||||
use App\Event\DispositionDeletedEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
@@ -27,6 +28,7 @@ class StaffingStatusSubscriber implements EventSubscriberInterface
|
||||
ApplicationStatusEvent::NAME => 'onApplicationStatusUpdated',
|
||||
DispositionCreatedEvent::NAME => 'onDispositionCreated',
|
||||
DispositionDeletedEvent::NAME => 'onDispositionDeleted',
|
||||
DispositionCalledOffEvent::NAME => 'onDispositionCalledOff',
|
||||
DocumentConfirmedEvent::NAME => 'onDocumentConfirmed',
|
||||
];
|
||||
}
|
||||
@@ -67,6 +69,13 @@ class StaffingStatusSubscriber implements EventSubscriberInterface
|
||||
$this->updateStaffingStatus($assignment);
|
||||
}
|
||||
|
||||
public function onDispositionCalledOff(DispositionCalledOffEvent $event): void
|
||||
{
|
||||
$assignment = $event->getDisposition()->getAssignment();
|
||||
|
||||
$this->updateStaffingStatus($assignment);
|
||||
}
|
||||
|
||||
public function onDocumentConfirmed(DocumentConfirmedEvent $event): void
|
||||
{
|
||||
$document = $event->getDocument();
|
||||
@@ -87,4 +96,4 @@ class StaffingStatusSubscriber implements EventSubscriberInterface
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Disposition;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
class DispositionCallOffType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('calledOffBy', ChoiceType::class, [
|
||||
'label' => 'abgesagt durch',
|
||||
'choices' => [
|
||||
'Teamer:in' => Disposition::CALLED_OFF_BY_TEAMER,
|
||||
'E&P' => Disposition::CALLED_OFF_BY_OFFICE,
|
||||
],
|
||||
])
|
||||
->add('calledOffReason', TextareaType::class, [
|
||||
'label' => 'Begründung',
|
||||
'attr' => [
|
||||
'data-controller' => 'textarea-autosize',
|
||||
'data-action' => 'textarea-autosize#resize',
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank([
|
||||
'message' => 'Bitte gib die Begründung ein',
|
||||
]),
|
||||
],
|
||||
])
|
||||
->add('sendNotification', CheckboxType::class, [
|
||||
'label' => 'Benachrichtigung mit Begründung versenden',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Disposition::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class DispositionVoter extends Voter
|
||||
public const CONTRACT_SUPPLEMENTARY = 'CONTRACT_SUPPLEMENTARY';
|
||||
public const INVOICE = 'INVOICE';
|
||||
public const FEEDBACK = 'FEEDBACK';
|
||||
public const CALL_OFF = 'CALL_OFF';
|
||||
|
||||
public function __construct(private readonly Security $security)
|
||||
{
|
||||
@@ -37,6 +38,7 @@ class DispositionVoter extends Voter
|
||||
static::CONTRACT_SUPPLEMENTARY,
|
||||
static::INVOICE,
|
||||
static::FEEDBACK,
|
||||
static::CALL_OFF,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -53,6 +55,8 @@ class DispositionVoter extends Voter
|
||||
static::FEEDBACK => $this->security->isGranted('ROLE_ADMINISTRATIVE')
|
||||
|| $this->assertHouseManagerAccess($token, $disposition),
|
||||
static::CONTRACT_SUPPLEMENTARY => $this->assertContractUploadAllowed($disposition),
|
||||
static::CALL_OFF => $this->security->isGranted('ROLE_ADMINISTRATIVE')
|
||||
&& Disposition::STATUS_CALLED_OFF !== $disposition->getStatus(),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
@@ -63,6 +67,10 @@ class DispositionVoter extends Voter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Disposition::STATUS_CALLED_OFF === $disposition->getStatus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $token->getUser();
|
||||
|
||||
@@ -95,6 +103,10 @@ class DispositionVoter extends Voter
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Disposition::STATUS_CALLED_OFF === $disposition->getStatus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null === $disposition->getDocumentByType(Upload::TYPE_CONTRACT);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user