fix: called-off dispositions no longer block teamers
This commit is contained in:
@@ -32,9 +32,12 @@ class CreateController extends AbstractController
|
|||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
$teamer = $user->getTeamer();
|
$teamer = $user->getTeamer();
|
||||||
|
|
||||||
// Only allow applications on empty slots
|
// Only allow applications on empty slots. The teamer list already hides assignments without
|
||||||
$availableSlots = (int) $assignment->getAvailableDispositions();
|
// a free slot, so this only catches the way past it: a bookmark or a direct link. Called-off
|
||||||
if (0 > $availableSlots) {
|
// dispositions do not occupy a slot, and an assignment without a slot count is treated as
|
||||||
|
// full - both the same way App\Repository\AssignmentRepository decides what to list.
|
||||||
|
$availableSlots = (int) $assignment->getAvailableDispositions() - $assignment->getActiveDispositions()->count();
|
||||||
|
if (0 >= $availableSlots) {
|
||||||
$this->addFlash('error', 'Auf diesem Einsatz sind bereits alle Plätze belegt');
|
$this->addFlash('error', 'Auf diesem Einsatz sind bereits alle Plätze belegt');
|
||||||
|
|
||||||
return $this->redirectToRoute('app_teamer_index');
|
return $this->redirectToRoute('app_teamer_index');
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use App\Controller\Traits\ReturnUrlTrait;
|
|||||||
use App\Entity\Assignment;
|
use App\Entity\Assignment;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Repository\ApplicationRepository;
|
use App\Repository\ApplicationRepository;
|
||||||
use App\Repository\DispositionRepository;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -19,7 +18,6 @@ class DetailController extends AbstractController
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ApplicationRepository $applicationRepository,
|
private readonly ApplicationRepository $applicationRepository,
|
||||||
private readonly DispositionRepository $dispositionRepository,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,13 +37,7 @@ class DetailController extends AbstractController
|
|||||||
])
|
])
|
||||||
;
|
;
|
||||||
|
|
||||||
$disposition = $this
|
$disposition = $assignment->getActiveDispositionByTeamer($teamer);
|
||||||
->dispositionRepository
|
|
||||||
->findOneBy([
|
|
||||||
'assignment' => $assignment,
|
|
||||||
'teamer' => $teamer,
|
|
||||||
])
|
|
||||||
;
|
|
||||||
|
|
||||||
$isBookmarked = $teamer->getBookmarks()->contains($assignment);
|
$isBookmarked = $teamer->getBookmarks()->contains($assignment);
|
||||||
|
|
||||||
|
|||||||
@@ -497,6 +497,24 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deliberately skips called-off dispositions: once a teamer has been called off - by themselves
|
||||||
|
* or by the office - the assignment is open for them again, so the teamer views have to offer
|
||||||
|
* the application instead of reporting them as staffed.
|
||||||
|
*/
|
||||||
|
public function getActiveDispositionByTeamer(Teamer $teamer): ?Disposition
|
||||||
|
{
|
||||||
|
$dispositions = $this->getActiveDispositions()->filter(function (Disposition $disposition) use ($teamer) {
|
||||||
|
return $disposition->getTeamer() === $teamer;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (0 === count($dispositions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dispositions->first();
|
||||||
|
}
|
||||||
|
|
||||||
public function getActiveDispositions(): Collection
|
public function getActiveDispositions(): Collection
|
||||||
{
|
{
|
||||||
return $this->dispositions->filter(function (Disposition $disposition) {
|
return $this->dispositions->filter(function (Disposition $disposition) {
|
||||||
|
|||||||
@@ -372,12 +372,31 @@ class AssignmentRepository extends ServiceEntityRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function getAvailableAssignmentsIds(): array
|
private function getAvailableAssignmentsIds(): array
|
||||||
|
{
|
||||||
|
$availableAssignments = $this
|
||||||
|
->getAvailableAssignmentsIdsQuery()
|
||||||
|
->getArrayResult()
|
||||||
|
;
|
||||||
|
|
||||||
|
return array_column($availableAssignments, 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A called-off disposition does not occupy a slot any more: the assignment has to show up as
|
||||||
|
* available again - both for the teamer who was called off and for everybody else.
|
||||||
|
*/
|
||||||
|
public function getAvailableAssignmentsIdsQuery(): Query
|
||||||
{
|
{
|
||||||
$qb = $this->createQueryBuilder('assignment');
|
$qb = $this->createQueryBuilder('assignment');
|
||||||
|
|
||||||
$availableAssignments = $qb
|
return $qb
|
||||||
->select('assignment.id', 'assignment.availableDispositions')
|
->select('assignment.id', 'assignment.availableDispositions')
|
||||||
->leftJoin('assignment.dispositions', 'disposition')
|
->leftJoin(
|
||||||
|
'assignment.dispositions',
|
||||||
|
'disposition',
|
||||||
|
Join::WITH,
|
||||||
|
$qb->expr()->neq('disposition.status', ':dispositionCalledOff')
|
||||||
|
)
|
||||||
->innerJoin('assignment.destination', 'destination')
|
->innerJoin('assignment.destination', 'destination')
|
||||||
->where($qb->expr()->andX(
|
->where($qb->expr()->andX(
|
||||||
$qb->expr()->orX(
|
$qb->expr()->orX(
|
||||||
@@ -393,12 +412,10 @@ class AssignmentRepository extends ServiceEntityRepository
|
|||||||
->groupBy('assignment.id', 'assignment.availableDispositions')
|
->groupBy('assignment.id', 'assignment.availableDispositions')
|
||||||
->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, Assignment::STATUS_CALLED_OFF])
|
->setParameter('status', [Assignment::STATUS_DRAFT, Assignment::STATUS_CALLED_OFF])
|
||||||
|
->setParameter('dispositionCalledOff', Disposition::STATUS_CALLED_OFF)
|
||||||
->setParameter('now', new \DateTimeImmutable())
|
->setParameter('now', new \DateTimeImmutable())
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getArrayResult()
|
|
||||||
;
|
;
|
||||||
|
|
||||||
return array_column($availableAssignments, 'id');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSelectableHotelCodes(): array
|
public function getSelectableHotelCodes(): array
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{% set application = assignment.applications.first %}
|
{% set application = assignment.applications.first %}
|
||||||
{% set disposition = assignment.dispositions.first%}
|
{% set disposition = assignment.activeDispositionByTeamer(teamer) %}
|
||||||
{% if application %}
|
{% if application %}
|
||||||
{% if application.status == constant('App\\Entity\\Application::STATUS_REJECTED') %}
|
{% if application.status == constant('App\\Entity\\Application::STATUS_REJECTED') %}
|
||||||
<a href="{{ path('app_teamer_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
|
<a href="{{ path('app_teamer_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
|
||||||
|
|||||||
@@ -35,15 +35,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{% set disposition = assignment.activeDispositionByTeamer(teamer) %}
|
||||||
{% if assignment.applications|length %}
|
{% if assignment.applications|length %}
|
||||||
{% set icon = 'hourglass' %}
|
{% set icon = 'hourglass' %}
|
||||||
{% set class = 'bg-yellow-100 text-yellow-800 hover:bg-yellow-50' %}
|
{% set class = 'bg-yellow-100 text-yellow-800 hover:bg-yellow-50' %}
|
||||||
{% set url = path('app_teamer_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) %}
|
{% set url = path('app_teamer_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) %}
|
||||||
{% set label = 'in Bearbeitung' %}
|
{% set label = 'in Bearbeitung' %}
|
||||||
{% elseif assignment.dispositions|length %}
|
{% elseif disposition %}
|
||||||
{% set icon = 'check' %}
|
{% set icon = 'check' %}
|
||||||
{% set class = 'bg-green-100 text-green-700 hover:bg-green-50' %}
|
{% set class = 'bg-green-100 text-green-700 hover:bg-green-50' %}
|
||||||
{% set url = path('app_teamer_disposition_detail', { 'uuid': assignment.dispositions[0].uuid }) %}
|
{% set url = path('app_teamer_disposition_detail', { 'uuid': disposition.uuid }) %}
|
||||||
{% set label = 'eingeteilt' %}
|
{% set label = 'eingeteilt' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set icon = 'hand' %}
|
{% set icon = 'hand' %}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Application;
|
||||||
|
use App\Entity\Assignment;
|
||||||
|
use App\Entity\Disposition;
|
||||||
|
use App\Entity\Teamer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A called-off teamer is free again: the teamer views ask the assignment for the teamer's
|
||||||
|
* disposition to decide between "eingeteilt" and the application button, so a called-off
|
||||||
|
* disposition must not be handed out there.
|
||||||
|
*/
|
||||||
|
class AssignmentTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testTheActiveDispositionOfATeamerIsReturned(): void
|
||||||
|
{
|
||||||
|
$assignment = new Assignment();
|
||||||
|
$teamer = new Teamer();
|
||||||
|
$disposition = $this->createDisposition($assignment, $teamer);
|
||||||
|
|
||||||
|
$this->assertSame($disposition, $assignment->getActiveDispositionByTeamer($teamer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testACalledOffDispositionIsNotReturned(): void
|
||||||
|
{
|
||||||
|
$assignment = new Assignment();
|
||||||
|
$teamer = new Teamer();
|
||||||
|
|
||||||
|
$this
|
||||||
|
->createDisposition($assignment, $teamer)
|
||||||
|
->setStatus(Disposition::STATUS_CALLED_OFF)
|
||||||
|
;
|
||||||
|
|
||||||
|
$this->assertNull($assignment->getActiveDispositionByTeamer($teamer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTheDispositionOfAnotherTeamerIsNotReturned(): void
|
||||||
|
{
|
||||||
|
$assignment = new Assignment();
|
||||||
|
$this->createDisposition($assignment, new Teamer());
|
||||||
|
|
||||||
|
$this->assertNull($assignment->getActiveDispositionByTeamer(new Teamer()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A teamer can be called off and disposed again on the same assignment, so both dispositions
|
||||||
|
* live side by side - the active one has to win.
|
||||||
|
*/
|
||||||
|
public function testTheActiveDispositionWinsOverACalledOffOne(): void
|
||||||
|
{
|
||||||
|
$assignment = new Assignment();
|
||||||
|
$teamer = new Teamer();
|
||||||
|
|
||||||
|
$this
|
||||||
|
->createDisposition($assignment, $teamer)
|
||||||
|
->setStatus(Disposition::STATUS_CALLED_OFF)
|
||||||
|
;
|
||||||
|
$disposition = $this->createDisposition($assignment, $teamer);
|
||||||
|
|
||||||
|
$this->assertSame($disposition, $assignment->getActiveDispositionByTeamer($teamer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createDisposition(Assignment $assignment, Teamer $teamer): Disposition
|
||||||
|
{
|
||||||
|
$disposition = new Disposition(new Application($assignment, $teamer));
|
||||||
|
$assignment->addDisposition($disposition);
|
||||||
|
|
||||||
|
return $disposition;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Assignment;
|
||||||
|
use App\Entity\Disposition;
|
||||||
|
use App\Repository\AssignmentRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Query;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The slot counting behind every teamer facing assignment list lives in DQL, so the query is
|
||||||
|
* compiled here and the parts carrying the semantics are pinned.
|
||||||
|
*/
|
||||||
|
class AssignmentRepositoryTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
public function testAvailableAssignmentsQueryIgnoresCalledOffDispositions(): void
|
||||||
|
{
|
||||||
|
$dql = $this->createAvailableAssignmentsIdsQuery()->getDQL();
|
||||||
|
|
||||||
|
// the exclusion has to live in the JOIN condition: moved into the WHERE clause it would
|
||||||
|
// drop assignments without any disposition at all
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'LEFT JOIN assignment.dispositions disposition WITH disposition.status <> :dispositionCalledOff',
|
||||||
|
$dql
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
'HAVING assignment.availableDispositions > COUNT(disposition.id)',
|
||||||
|
$dql
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAvailableAssignmentsQueryBindsTheCalledOffStatuses(): void
|
||||||
|
{
|
||||||
|
$query = $this->createAvailableAssignmentsIdsQuery();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
Disposition::STATUS_CALLED_OFF,
|
||||||
|
$query->getParameter('dispositionCalledOff')->getValue()
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
[Assignment::STATUS_DRAFT, Assignment::STATUS_CALLED_OFF],
|
||||||
|
$query->getParameter('status')->getValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAvailableAssignmentsIdsQuery(): Query
|
||||||
|
{
|
||||||
|
/** @var EntityManagerInterface $entityManager */
|
||||||
|
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
/** @var AssignmentRepository $repository */
|
||||||
|
$repository = $entityManager->getRepository(Assignment::class);
|
||||||
|
|
||||||
|
return $repository->getAvailableAssignmentsIdsQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user