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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
||||||
</symbol>
|
</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">
|
<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 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" />
|
<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_STAFFING = 'staffing';
|
||||||
public const STATUS_UNSTAFFED = 'unstaffed';
|
public const STATUS_UNSTAFFED = 'unstaffed';
|
||||||
public const STATUS_DELETED = 'deleted';
|
public const STATUS_DELETED = 'deleted';
|
||||||
|
public const STATUS_CALLED_OFF = 'called_off';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[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\Application;
|
||||||
use App\Entity\Upload;
|
use App\Entity\Upload;
|
||||||
use App\Event\ApplicationStatusEvent;
|
use App\Event\ApplicationStatusEvent;
|
||||||
|
use App\Event\AssignmentCalledOffEvent;
|
||||||
use App\Event\DispositionCreatedEvent;
|
use App\Event\DispositionCreatedEvent;
|
||||||
use App\Event\DocumentRejectedEvent;
|
use App\Event\DocumentRejectedEvent;
|
||||||
use App\Event\DocumentUploadedEvent;
|
use App\Event\DocumentUploadedEvent;
|
||||||
@@ -30,6 +31,7 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
|||||||
DocumentUploadedEvent::NAME => 'onDocumentUploaded',
|
DocumentUploadedEvent::NAME => 'onDocumentUploaded',
|
||||||
DocumentRejectedEvent::NAME => 'onDocumentRejected',
|
DocumentRejectedEvent::NAME => 'onDocumentRejected',
|
||||||
ApplicationStatusEvent::NAME => 'onApplicationStatus',
|
ApplicationStatusEvent::NAME => 'onApplicationStatus',
|
||||||
|
AssignmentCalledOffEvent::NAME => 'onAssignmentCalledOff',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,4 +156,20 @@ class EmailNotificationSubscriber implements EventSubscriberInterface
|
|||||||
'template' => 'email/application_rejected.html.twig',
|
'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' => [
|
'choices' => [
|
||||||
'Entwurf' => Assignment::STATUS_DRAFT,
|
'Entwurf' => Assignment::STATUS_DRAFT,
|
||||||
'veröffentlicht' => Assignment::STATUS_PUBLISHED,
|
'veröffentlicht' => Assignment::STATUS_PUBLISHED,
|
||||||
|
'abgesagt' => Assignment::STATUS_CALLED_OFF,
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->add('availableDispositions', IntegerType::class, [
|
->add('availableDispositions', IntegerType::class, [
|
||||||
|
|||||||
@@ -67,12 +67,14 @@ class AssignmentRepository extends ServiceEntityRepository
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
$qb->expr()->isNull('assignment.deletedAt'),
|
$qb->expr()->isNull('assignment.deletedAt'),
|
||||||
|
$qb->expr()->notIn('assignment.status', ':status'),
|
||||||
$qb->expr()->in('assignment.id', $availableAssignmentsIds)
|
$qb->expr()->in('assignment.id', $availableAssignmentsIds)
|
||||||
))
|
))
|
||||||
->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer'))
|
->leftJoin('assignment.applications', 'application', Join::WITH, $qb->expr()->eq('application.teamer', ':teamer'))
|
||||||
->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer'))
|
->leftJoin('assignment.dispositions', 'disposition', Join::WITH, $qb->expr()->eq('disposition.teamer', ':teamer'))
|
||||||
->groupBy('assignment')
|
->groupBy('assignment')
|
||||||
->setParameter('teamer', $teamer)
|
->setParameter('teamer', $teamer)
|
||||||
|
->setParameter('status', [Assignment::STATUS_DRAFT, Assignment::STATUS_CALLED_OFF])
|
||||||
->setParameter('now', new \DateTimeImmutable())
|
->setParameter('now', new \DateTimeImmutable())
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -273,12 +275,12 @@ class AssignmentRepository extends ServiceEntityRepository
|
|||||||
$qb->expr()->gt('assignment.dateFrom', ':now')
|
$qb->expr()->gt('assignment.dateFrom', ':now')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
$qb->expr()->neq('assignment.status', ':status'),
|
$qb->expr()->notIn('assignment.status', ':status'),
|
||||||
$qb->expr()->isNull('assignment.deletedAt')
|
$qb->expr()->isNull('assignment.deletedAt')
|
||||||
))
|
))
|
||||||
->groupBy('assignment')
|
->groupBy('assignment')
|
||||||
->having($qb->expr()->gt('assignment.availableDispositions', $qb->expr()->count('disposition.id')))
|
->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())
|
->setParameter('now', new \DateTimeImmutable())
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getArrayResult()
|
->getArrayResult()
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class DispositionRepository extends ServiceEntityRepository
|
|||||||
->innerJoin('assignment.destination', 'destination')
|
->innerJoin('assignment.destination', 'destination')
|
||||||
->where($qb->expr()->andX(
|
->where($qb->expr()->andX(
|
||||||
$qb->expr()->eq('disposition.teamer', ':teamer'),
|
$qb->expr()->eq('disposition.teamer', ':teamer'),
|
||||||
|
$qb->expr()->neq('assignment.status', ':status'),
|
||||||
$qb->expr()->orX(
|
$qb->expr()->orX(
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->isNull('assignment.dateFrom'),
|
$qb->expr()->isNull('assignment.dateFrom'),
|
||||||
@@ -49,6 +50,7 @@ class DispositionRepository extends ServiceEntityRepository
|
|||||||
)
|
)
|
||||||
))
|
))
|
||||||
->setParameter('teamer', $teamer)
|
->setParameter('teamer', $teamer)
|
||||||
|
->setParameter('status', Assignment::STATUS_CALLED_OFF)
|
||||||
->setParameter('date', new \DateTimeImmutable())
|
->setParameter('date', new \DateTimeImmutable())
|
||||||
->getQuery()
|
->getQuery()
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class AssignmentVoter extends Voter
|
|||||||
public const EDIT = 'EDIT';
|
public const EDIT = 'EDIT';
|
||||||
public const APPLY = 'APPLY';
|
public const APPLY = 'APPLY';
|
||||||
public const PUBLISH = 'PUBLISH';
|
public const PUBLISH = 'PUBLISH';
|
||||||
|
public const CALL_OFF = 'CALL_OFF';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
@@ -30,7 +31,7 @@ class AssignmentVoter extends Voter
|
|||||||
return false;
|
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
|
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||||
@@ -54,8 +55,19 @@ class AssignmentVoter extends Voter
|
|||||||
&& $this->security->isGranted('ROLE_ADMINISTRATIVE');
|
&& $this->security->isGranted('ROLE_ADMINISTRATIVE');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow applications to published assignments
|
// Only users with administrative role may call off assignments
|
||||||
if (Assignment::STATUS_DRAFT === $assignment->getStatus()) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,26 +71,26 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex flex-col items-center space-y-2">
|
<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') }}
|
{{ 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 %}
|
{% else %}
|
||||||
{% if assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFED') %}
|
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-gray-100">
|
||||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-green-500">
|
<circle cx="3" cy="3" r="2" />
|
||||||
<circle cx="3" cy="3" r="2" />
|
</svg>
|
||||||
</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 %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -167,6 +167,17 @@
|
|||||||
hx-swap="beforeend">
|
hx-swap="beforeend">
|
||||||
löschen
|
löschen
|
||||||
</button>
|
</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() }) }}"
|
<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"
|
class="text-gray-700 hover:text-primary block px-4 py-2 text-sm"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
@@ -207,6 +218,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2 pb-8 -mt-4">
|
<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">
|
<div class="flex items-center">
|
||||||
<svg viewBox="0 0 5 5" class="h-4 w-4 fill-current text-gray-100 mr-1">
|
<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" />
|
<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