Files
myep-team/src/Service/Assignment/TimelineService.php
T

123 lines
4.5 KiB
PHP

<?php
namespace App\Service\Assignment;
use App\Entity\Assignment;
use App\Model\TimelineItem;
use Carbon\CarbonPeriod;
use function Symfony\Component\String\b;
/**
* Service for processing assignments into timeline visualization data.
* Organizes assignments and dispositions by hotel in a timeline format,
* handling overlapping dates by distributing items across multiple rows.
*/
class TimelineService
{
/**
* Preprocesses assignments into a timeline structure organized by hotel.
* Creates timeline items for both vacant assignment slots and actual dispositions,
* distributing them across multiple rows to avoid date overlaps.
*
* @param Assignment[] $assignments Array of assignments to process
* @return array Multi-dimensional array: [hotelCode][rowIndex][itemKey] => 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);
}
}