254 lines
10 KiB
PHP
254 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Service;
|
|
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
|
|
|
/**
|
|
* Handles processing of the assignedRoomId field for booking participants.
|
|
*
|
|
* This handler manages room assignments for participants in the booking creation
|
|
* process. It processes the assignedRoomId field from form submissions and updates
|
|
* the participant DTO with the selected room. The handler properly handles empty
|
|
* selections (converting them to null) and validates numeric room IDs.
|
|
*
|
|
* Field Processing:
|
|
* - Extracts room ID from submitted form data
|
|
* - Converts empty strings to null (unselected choice)
|
|
* - Normalizes string values to integers
|
|
* - Detects capacity conflicts when assigning rooms
|
|
* - Automatically unassigns minimum number of participants to resolve conflicts
|
|
* - Generates notifications for unassigned participants
|
|
* - Updates the participant's assignedRoomId property
|
|
*
|
|
* Conflict Resolution Strategy:
|
|
* - Allows participants to select any room from Step 1 selections
|
|
* - When assignment would exceed capacity, automatically unassigns others
|
|
* - Unassignment priority: highest index participants first (keeps applicant stable)
|
|
* - Only unassigns minimum number needed to make space
|
|
*
|
|
* Dependencies: None (this is a base field that other handlers may depend on)
|
|
*/
|
|
class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandler
|
|
{
|
|
/**
|
|
* Returns the form field name this handler processes.
|
|
*
|
|
* This handler is responsible for the 'assignedRoomId' field, which contains
|
|
* the selected room ID for each participant in the booking form.
|
|
*
|
|
* @return string The field name 'assignedRoomId'
|
|
*/
|
|
public function getFieldName(): string
|
|
{
|
|
return 'assignedRoomId';
|
|
}
|
|
|
|
/**
|
|
* Processes the assignedRoomId field for a specific participant.
|
|
*
|
|
* This method extracts the room assignment from the submitted form data and
|
|
* updates the corresponding participant in the booking DTO. It handles the
|
|
* common form processing pattern where empty selections are submitted as
|
|
* empty strings but should be stored as null values.
|
|
*
|
|
* Processing steps:
|
|
* 1. Safely retrieves the participant object from the DTO
|
|
* 2. Extracts the assignedRoomId value from submitted data
|
|
* 3. Normalizes the value (empty string → null, numeric string → integer)
|
|
* 4. Detects capacity conflicts when assigning to room
|
|
* 5. Resolves conflicts by auto-unassigning minimum participants needed
|
|
* 6. Updates the participant's assignedRoomId property
|
|
*
|
|
* @param array<string, mixed> $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<int, \App\Form\Model\ParticipantDto> 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';
|
|
}
|
|
}
|