feat: assignment call off

This commit is contained in:
Björn Fromme
2024-11-06 21:04:33 +01:00
parent ca0c965058
commit ef7871f1d4
12 changed files with 181 additions and 23 deletions
+3
View File
@@ -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,
]);
}
}
+1
View File
@@ -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]
+19
View File
@@ -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',
]);
}
}
}
+1
View File
@@ -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, [
+4 -2
View File
@@ -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()
+2
View File
@@ -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()
;
+15 -3
View File
@@ -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,10 +71,11 @@
</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') }}
{% else %}
{% if assignment.staffingStatus == constant('App\\Entity\\Assignment::STATUS_STAFFED') %}
{% 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>
@@ -91,7 +92,6 @@
<circle cx="3" cy="3" r="2" />
</svg>
{% endif %}
{% endif %}
</div>
</td>
<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 %}