feat: assignment call off
This commit is contained in:
@@ -166,6 +166,9 @@
|
||||
<symbol id="icon-power" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
||||
</symbol>
|
||||
<symbol id="icon-no-symbol" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</symbol>
|
||||
<symbol id="icon-bus" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24" stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Administrative\Assignment;
|
||||
|
||||
use App\Entity\Assignment;
|
||||
use App\Event\AssignmentCalledOffEvent;
|
||||
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/assignment/call-off/{uuid}', name: 'app_administrative_assignment_call_off')]
|
||||
#[IsGranted('ROLE_ADMINISTRATIVE')]
|
||||
public function index(Assignment $assignment, Request $request): Response
|
||||
{
|
||||
if (true === $request->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Assignment;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class AssignmentCalledOffEvent extends Event
|
||||
{
|
||||
public const NAME = 'assignment.called_off';
|
||||
|
||||
public function __construct(private readonly Assignment $assignment)
|
||||
{}
|
||||
|
||||
public function getAssignment(): Assignment
|
||||
{
|
||||
return $this->assignment;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,26 +71,26 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
{% 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') %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-green-500">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_PARTLY_STAFFED') %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-yellow-500">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFING') %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-blue-500">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% else %}
|
||||
{% if assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFED') %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-green-500">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_PARTLY_STAFFED') %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-yellow-500">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% elseif assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFING') %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-blue-500">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-gray-100">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-gray-100">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@@ -167,6 +167,17 @@
|
||||
hx-swap="beforeend">
|
||||
löschen
|
||||
</button>
|
||||
{% if is_granted('CALL_OFF', assignment) %}
|
||||
<button type="button"
|
||||
class="text-red-500 hover:text-primary block px-4 py-2 text-sm w-full text-left"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
hx-get="{{ path('app_administrative_assignment_call_off', { 'uuid': assignment.uuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
absagen
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{{ path('app_administrative_assignment_duplicate', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
|
||||
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||
role="menuitem"
|
||||
@@ -207,6 +218,10 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 pb-8 -mt-4">
|
||||
<div class="flex items-center">
|
||||
{{ icon('no-symbol', 'w-4 h-4 mr-1') }}
|
||||
<span>= abgesagt</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-gray-100 mr-1">
|
||||
<circle cx="3" cy="3" r="2" />
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
Möchtest du den Einsatz/die Reise <em>{{ assignment.destination.product }}</em> wirklich absagen und die
|
||||
eingeteilten Teamer:innen benachrichtigen?
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends 'email/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>
|
||||
Hallo aus Köln,
|
||||
</h1>
|
||||
<p>
|
||||
leider mussten wir die Reise zu deinem Einsatz '{{ assignment.destination }}' absagen, sodass dein Einsatz
|
||||
nicht stattfinden kann.
|
||||
</p>
|
||||
<p>
|
||||
Bitte entschuldige die Umstände!
|
||||
</p>
|
||||
<p>
|
||||
Bei Fragen melde dich gerne unter <a href="mailto:[email protected]">[email protected]</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('app_teamer_index') }}" class="button">
|
||||
Zum Portal
|
||||
</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user