diff --git a/src/Controller/Teamer/Application/CreateController.php b/src/Controller/Teamer/Application/CreateController.php index 837712b..a00ca56 100644 --- a/src/Controller/Teamer/Application/CreateController.php +++ b/src/Controller/Teamer/Application/CreateController.php @@ -32,9 +32,12 @@ class CreateController extends AbstractController $user = $this->getUser(); $teamer = $user->getTeamer(); - // Only allow applications on empty slots - $availableSlots = (int) $assignment->getAvailableDispositions(); - if (0 > $availableSlots) { + // Only allow applications on empty slots. The teamer list already hides assignments without + // a free slot, so this only catches the way past it: a bookmark or a direct link. Called-off + // 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'); return $this->redirectToRoute('app_teamer_index'); diff --git a/src/Controller/Teamer/Assignment/DetailController.php b/src/Controller/Teamer/Assignment/DetailController.php index c89da23..d3f2fb5 100644 --- a/src/Controller/Teamer/Assignment/DetailController.php +++ b/src/Controller/Teamer/Assignment/DetailController.php @@ -6,7 +6,6 @@ use App\Controller\Traits\ReturnUrlTrait; use App\Entity\Assignment; use App\Entity\User; use App\Repository\ApplicationRepository; -use App\Repository\DispositionRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -19,7 +18,6 @@ class DetailController extends AbstractController public function __construct( private readonly ApplicationRepository $applicationRepository, - private readonly DispositionRepository $dispositionRepository, ) { } @@ -39,13 +37,7 @@ class DetailController extends AbstractController ]) ; - $disposition = $this - ->dispositionRepository - ->findOneBy([ - 'assignment' => $assignment, - 'teamer' => $teamer, - ]) - ; + $disposition = $assignment->getActiveDispositionByTeamer($teamer); $isBookmarked = $teamer->getBookmarks()->contains($assignment); diff --git a/src/Entity/Assignment.php b/src/Entity/Assignment.php index 8420aa8..3fa672d 100644 --- a/src/Entity/Assignment.php +++ b/src/Entity/Assignment.php @@ -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 { return $this->dispositions->filter(function (Disposition $disposition) { diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 43b66a9..b65bb80 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -372,12 +372,31 @@ class AssignmentRepository extends ServiceEntityRepository } 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'); - $availableAssignments = $qb + return $qb ->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') ->where($qb->expr()->andX( $qb->expr()->orX( @@ -393,12 +412,10 @@ class AssignmentRepository extends ServiceEntityRepository ->groupBy('assignment.id', 'assignment.availableDispositions') ->having($qb->expr()->gt('assignment.availableDispositions', $qb->expr()->count('disposition.id'))) ->setParameter('status', [Assignment::STATUS_DRAFT, Assignment::STATUS_CALLED_OFF]) + ->setParameter('dispositionCalledOff', Disposition::STATUS_CALLED_OFF) ->setParameter('now', new \DateTimeImmutable()) ->getQuery() - ->getArrayResult() ; - - return array_column($availableAssignments, 'id'); } public function getSelectableHotelCodes(): array diff --git a/templates/teamer/assignment/index.html.twig b/templates/teamer/assignment/index.html.twig index 4f0c9cd..9d6140e 100644 --- a/templates/teamer/assignment/index.html.twig +++ b/templates/teamer/assignment/index.html.twig @@ -58,7 +58,7 @@