$submittedData The submitted participant form data * @param BookingDto $bookingDto The booking DTO to update (create or edit) * @param int $participantIndex The index of the participant being processed */ public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void { // Safely get the participant object, returning early if not found $participant = $this->getParticipant($bookingDto, $participantIndex); if (null === $participant) { return; } // Extract the room ID from form data (defaults to null if not present) $roomId = $this->getFieldValue($submittedData, $this->getFieldName()); // Convert form string to integer, handling empty selections as null $normalizedRoomId = $this->normalizeIntValue($roomId); // If assigning to a room (not unselecting), check for capacity conflicts if (null !== $normalizedRoomId && $this->detectRoomCapacityConflict($normalizedRoomId, $bookingDto, $participantIndex)) { // Resolve conflict by auto-unassigning other participants $this->resolveRoomCapacityConflict($normalizedRoomId, $bookingDto, $participantIndex); } // Assign the room to the current participant $participant->assignedRoomId = $normalizedRoomId; } /** * Detects if assigning the participant to a room would exceed capacity. * * Calculates current occupancy for the room (excluding the current participant * if they're already assigned to it) and compares against total capacity. * Returns true if the assignment would cause a capacity conflict. * * @param int $roomId The room ID being assigned * @param BookingDto $bookingDto The booking DTO with all participants * @param int $currentParticipantIndex The participant being assigned * * @return bool True if assignment would exceed capacity */ private function detectRoomCapacityConflict(int $roomId, BookingDto $bookingDto, int $currentParticipantIndex): bool { // Get the room selection details $roomSelection = null; foreach ($bookingDto->roomSelections as $selection) { if ($selection->id === $roomId) { $roomSelection = $selection; break; } } // If room not found in selections, no conflict (shouldn't happen) if (null === $roomSelection) { return false; } // Calculate total capacity for this room type // In edit mode, use maxQuantity which includes available rooms beyond the booked count. // In create mode, use quantity which is the user's room selection from step 1. if (BookingDto::MODE_EDIT === $bookingDto->getMode()) { $effectiveQuantity = $roomSelection->maxQuantity; } else { $effectiveQuantity = $roomSelection->quantity ?? 0; } $totalCapacity = $effectiveQuantity * $roomSelection->capacity; // Count current occupancy (excluding current participant) $occupancy = 0; foreach ($bookingDto->participants as $index => $participant) { if ($index !== $currentParticipantIndex && $participant->assignedRoomId === $roomId) { ++$occupancy; } } // Conflict if assigning would exceed capacity return ($occupancy + 1) > $totalCapacity; } /** * Resolves room capacity conflicts by auto-unassigning participants. * * When a participant is assigned to a room at capacity, this method * automatically unassigns the minimum number of other participants * needed to make space. Unassignment priority is highest index first * (keeps applicant and early participants stable). * * Generates warning notifications for unassigned participants. * * @param int $roomId The room ID being assigned * @param BookingDto $bookingDto The booking DTO with all participants * @param int $currentParticipantIndex The participant being assigned */ private function resolveRoomCapacityConflict(int $roomId, BookingDto $bookingDto, int $currentParticipantIndex): void { // Get room label for notification message $roomLabel = $this->getRoomLabelById($roomId, $bookingDto); // Get participants currently assigned to this room (excluding current participant) $participantsWithRoom = $this->getParticipantsAssignedToRoom($roomId, $bookingDto, $currentParticipantIndex); // Sort by index descending (highest index first) krsort($participantsWithRoom); // Calculate how many need to be unassigned $roomSelection = null; foreach ($bookingDto->roomSelections as $selection) { if ($selection->id === $roomId) { $roomSelection = $selection; break; } } if (null === $roomSelection) { return; // Shouldn't happen } if (BookingDto::MODE_EDIT === $bookingDto->getMode()) { $effectiveQuantity = $roomSelection->maxQuantity; } else { $effectiveQuantity = $roomSelection->quantity ?? 0; } $totalCapacity = $effectiveQuantity * $roomSelection->capacity; $currentOccupancy = count($participantsWithRoom); $spacesNeeded = ($currentOccupancy + 1) - $totalCapacity; // Unassign minimum participants needed (typically 1) $unassignedCount = 0; foreach ($participantsWithRoom as $index => $participant) { if ($unassignedCount >= $spacesNeeded) { break; } // Unassign participant $participant->assignedRoomId = null; // Add notification for unassigned participant // For internal agency bookings, first participant is not the applicant $isApplicant = $participant->isApplicant() && false === $bookingDto->isInternalAgencyBooking(); $participantLabel = $isApplicant ? 'Anmelder:in' : 'Teilnehmer:in '.($participant->index + 1); $participant->addNotification('warning', sprintf('%s wurde von %s entfernt', $roomLabel, $participantLabel)); ++$unassignedCount; } } /** * Gets all participants assigned to a specific room. * * @param int $roomId The room ID to search for * @param BookingDto $bookingDto The booking DTO with all participants * @param int $excludeParticipantIndex Participant index to exclude from results * * @return array Array of participants indexed by their position */ private function getParticipantsAssignedToRoom(int $roomId, BookingDto $bookingDto, int $excludeParticipantIndex): array { $participantsWithRoom = []; foreach ($bookingDto->participants as $index => $participant) { if ($index !== $excludeParticipantIndex && $participant->assignedRoomId === $roomId) { $participantsWithRoom[$index] = $participant; } } return $participantsWithRoom; } /** * Gets the room label for a given room ID. * * @param int $roomId The room ID to look up * @param BookingDto $bookingDto The booking DTO with room selections * * @return string The room label or 'Unbekannt' if not found */ private function getRoomLabelById(int $roomId, BookingDto $bookingDto): string { foreach ($bookingDto->roomSelections as $selection) { if ($selection->id === $roomId) { return $selection->label; } } return 'Unbekannt'; } }