TimelineItem */ public function preprocess(array $assignments): array { $rows = []; foreach ($assignments as $assignment) { // exclude called off assignments // TODO: refactor filtering to do this on database level if (Assignment::STATUS_CALLED_OFF === $assignment->getStatus()) { continue; } $destination = $assignment->getDestination(); $hotelCode = $destination->getHotelCodeNormalized(); // add first row for current hotel unless existing if (false === isset($rows[$hotelCode])) { $rows[$hotelCode] = [[]]; } $dispositionsCount = $assignment->getDispositions()->count(); $dispositionsAvailable = $assignment->getAvailableDispositions(); // Handle assignments with no dispositions - create items for all available slots if (0 === $dispositionsCount) { $item = TimelineItem::fromAssignment($assignment); for ($i = 1; $i <= $assignment->getAvailableDispositions(); $i++) { $this->addItem($rows[$hotelCode], $item); } } else { // Create items for remaining vacant slots if any if ($dispositionsCount < $dispositionsAvailable) { $item = TimelineItem::fromAssignment($assignment); for ($i = 1; $i <= $dispositionsAvailable - $dispositionsCount; $i++) { $this->addItem($rows[$hotelCode], $item); } } // Create items for actual dispositions foreach ($assignment->getDispositions() as $disposition) { $item = TimelineItem::fromDisposition($disposition); $this->addItem($rows[$hotelCode], $item); } } } return $rows; } /** * Adds a timeline item to the appropriate row, ensuring no date overlaps. * Finds the first available row where the item doesn't overlap with existing items, * or creates a new row if necessary. * * @param array $rows Reference to the rows array for a specific hotel * @param TimelineItem $item The timeline item to add */ private function addItem(array &$rows, TimelineItem $item): void { $rowIndex = 0; $fitsRow = false; // Find first row where item doesn't overlap with existing items foreach ($rows as $index => $row) { $fitsRow = true; foreach ($row as $entry) { /** @var TimelineItem $entry */ // Check for date overlap: item overlaps if its end >= entry start AND item start <= entry end if ($entry->getDateTo() >= $item->getDateFrom() && $entry->getDateFrom() <= $item->getDateTo()) { $fitsRow = false; } } $rowIndex = $index; if (true === $fitsRow) { break; } } // If no existing row fits, create a new row if (false === $fitsRow) { ++$rowIndex; } $rows[$rowIndex][$item->getKey()] = $item; } /** * Creates a time window for timeline visualization. * Returns a period from 6 weeks ago to 6 months in the future, * providing context for both recent and upcoming assignments. * * @return CarbonPeriod The active time window for timeline display */ public function getActiveWindow(): CarbonPeriod { $dateFrom = (new \DateTimeImmutable())->modify('-6 weeks'); $dateTo = (new \DateTimeImmutable())->modify('+6 months'); return CarbonPeriod::create($dateFrom, $dateTo); } }