Lines
85.06%
188 / 221
Functions and Methods
77.77%
21 / 27
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| QueueList | 85.06% | 188 / 221 | 77.77% | 21 / 27 | 116.97 | 0.00% | 0 / 1 | |||
| setWaitingTimePreferences | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 2 | |||||
| setTransferedProcessList | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| getProcessTimeAverage | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| getWorkstationCount | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | |||||
| withEstimatedWaitingTime | 85.96% | 49 / 57 | 0.00% | 0 / 1 | 16.71 | |||||
| getSortPriority | 83.33% | 5 / 6 | 0.00% | 0 / 1 | 4.07 | |||||
| withWaitingTime | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | |||||
| withSortedArrival | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 1 | |||||
| withSortedWaitingTime | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| withAppointment | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | |||||
| withOutAppointment | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | |||||
| getEstimatedWaitingTime | 71.42% | 10 / 14 | 0.00% | 0 / 1 | 2.09 | |||||
| withFakeWaitingnumber | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 1 | |||||
| getFakeOrLastWaitingnumber | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | |||||
| getQueueByNumber | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 3 | |||||
| getNextProcess | 100.00% | 17 / 17 | 100.00% | 1 / 1 | 7 | |||||
| getQueuePositionByNumber | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | |||||
| getCountWithWaitingTime | 84.61% | 11 / 13 | 0.00% | 0 / 1 | 7.18 | |||||
| withStatus | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 4 | |||||
| withoutStatus | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 4 | |||||
| withShortNameDestinationHint | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 4 | |||||
| toProcessList | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | |||||
| withoutDublicates | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 3 | |||||
| getWaitingNumberList | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | |||||
| getWaitingNumberListCsv | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| withSelectedProcessFirst | 0.00% | 0 / 7 | 0.00% | 0 / 1 | 12 | |||||
| sortByCallTime | 0.00% | 0 / 11 | 0.00% | 0 / 1 | 20 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | namespace BO\Zmsentities\Collection; | |
| 4 | ||
| 5 | /** | |
| 6 | * @SuppressWarnings(Complexity) | |
| 7 | * @SuppressWarnings(PublicMethod) | |
| 8 | * | |
| 9 | * @extends Base<\BO\Zmsentities\Queue> | |
| 10 | */ | |
| 11 | class QueueList extends Base implements \BO\Zmsentities\Helper\NoSanitize | |
| 12 | { | |
| 13 | public const string ENTITY_CLASS = '\BO\Zmsentities\Queue'; | |
| 14 | ||
| 15 | public const int FAKE_WAITINGNUMBER = -1; | |
| 16 | ||
| 17 | public const array STATUS_IGNORE = ['called', 'processing', 'missed', 'parked']; | |
| 18 | ||
| 19 | public const array STATUS_APPEND = ['missed', 'parked']; | |
| 20 | ||
| 21 | public const array STATUS_CALLED = ['called', 'processing']; | |
| 22 | ||
| 23 | public const array STATUS_FAKE = ['fake']; | |
| 24 | ||
| 25 | public const int DEFAULT_PRIORITY_WITHOUT_APPOINTMENT = 3; | |
| 26 | ||
| 27 | public const int DEFAULT_PRIORITY_WITH_APPOINTMENT = 2; | |
| 28 | ||
| 29 | protected mixed $processTimeAverage = null; | |
| 30 | ||
| 31 | protected mixed $workstationCount = null; | |
| 32 | ||
| 33 | protected bool $fromProcessList = false; | |
| 34 | ||
| 35 | public function setWaitingTimePreferences(mixed $processTimeAverage, mixed $workstationCount): static | |
| 36 | { | |
| 37 | if ($processTimeAverage <= 0) { | |
| 38 | throw new \Exception("QueueList::withEstimatedWaitingTime() requires processTimeAverage"); | |
| 39 | } | |
| 40 | $this->processTimeAverage = $processTimeAverage; | |
| 41 | $this->workstationCount = $workstationCount; | |
| 42 | return $this; | |
| 43 | } | |
| 44 | ||
| 45 | public function setTransferedProcessList(bool $bool = true): static | |
| 46 | { | |
| 47 | $this->fromProcessList = $bool; | |
| 48 | return $this; | |
| 49 | } | |
| 50 | ||
| 51 | public function getProcessTimeAverage(): mixed | |
| 52 | { | |
| 53 | return $this->processTimeAverage; | |
| 54 | } | |
| 55 | ||
| 56 | public function getWorkstationCount(): mixed | |
| 57 | { | |
| 58 | return $this->workstationCount ? $this->workstationCount : 1; | |
| 59 | } | |
| 60 | ||
| 61 | public function withEstimatedWaitingTime( | |
| 62 | mixed $processTimeAverage, | |
| 63 | mixed $workstationCount, | |
| 64 | \DateTimeInterface $dateTime, | |
| 65 | bool $createFake = true | |
| 66 | ): \BO\Zmsentities\Collection\QueueList { | |
| 67 | $this->setWaitingTimePreferences($processTimeAverage, $workstationCount); | |
| 68 | $queueFull = $this->withWaitingTime($dateTime); | |
| 69 | $queueWithWaitingTime = $queueFull->withStatus(self::STATUS_CALLED); | |
| 70 | $queueAppend = $queueFull->withStatus(self::STATUS_APPEND); | |
| 71 | $queueFull = $queueFull->withoutStatus(self::STATUS_IGNORE); | |
| 72 | if ($createFake) { | |
| 73 | $queueFull = $queueFull->withFakeWaitingnumber($dateTime); | |
| 74 | } | |
| 75 | $listWithAppointment = $queueFull->withAppointment()->withSortedArrival()->getArrayCopy(); | |
| 76 | $listNoAppointment = $queueFull->withOutAppointment()->withSortedArrival()->getArrayCopy(); | |
| 77 | $nextWithAppointment = array_shift($listWithAppointment); | |
| 78 | $nextNoAppointment = array_shift($listNoAppointment); | |
| 79 | $currentTime = $dateTime->getTimestamp() + 120; | |
| 80 | $optimisticTime = $dateTime->getTimestamp(); | |
| 81 | $pessimisticTime = $dateTime->getTimestamp(); | |
| 82 | ||
| 83 | $waitingTime = 0; | |
| 84 | $waitingTimeOpt = 0; | |
| 85 | $timeSlot = ($workstationCount) ? $processTimeAverage * 60 / $workstationCount : $processTimeAverage * 60; | |
| 86 | $timeSlotOptimistic = $timeSlot * 0.8; | |
| 87 | $timeSlotPessimistic = $timeSlot * 1.0; | |
| 88 | $workstationSkip = ceil($workstationCount * 1.0); | |
| 89 | $pessimisticTime += $timeSlot; | |
| 90 | $waitingTimePes = (int)floor(($pessimisticTime - $dateTime->getTimestamp()) / 60); | |
| 91 | while ($nextWithAppointment || $nextNoAppointment) { | |
| 92 | if ($nextNoAppointment && (int) $nextNoAppointment->priority === 1) { | |
| 93 | $nextNoAppointment->waitingTimeEstimate = $waitingTimePes; | |
| 94 | $nextNoAppointment->waitingTimeOptimistic = $waitingTimeOpt; | |
| 95 | $queueWithWaitingTime->addEntity($nextNoAppointment); | |
| 96 | $nextNoAppointment = array_shift($listNoAppointment); | |
| 97 | } elseif ( | |
| 98 | $nextNoAppointment | |
| 99 | && $nextWithAppointment | |
| 100 | && (int) $nextNoAppointment->priority === 2 | |
| 101 | && $nextNoAppointment->arrivalTime < $nextWithAppointment->arrivalTime | |
| 102 | ) { | |
| 103 | $nextNoAppointment->waitingTimeEstimate = $waitingTimePes; | |
| 104 | $nextNoAppointment->waitingTimeOptimistic = $waitingTimeOpt; | |
| 105 | $queueWithWaitingTime->addEntity($nextNoAppointment); | |
| 106 | $nextNoAppointment = array_shift($listNoAppointment); | |
| 107 | } elseif ($nextWithAppointment && $currentTime >= $nextWithAppointment->arrivalTime) { | |
| 108 | $nextWithAppointment->waitingTimeEstimate = $waitingTime + 1; | |
| 109 | $nextWithAppointment->waitingTimeOptimistic = | |
| 110 | floor(($nextWithAppointment->arrivalTime - $dateTime->getTimestamp()) / 60); | |
| 111 | if ($optimisticTime >= $nextWithAppointment->arrivalTime) { | |
| 112 | $nextWithAppointment->waitingTimeOptimistic = $waitingTimeOpt; | |
| 113 | $nextWithAppointment->waitingTimeEstimate = $waitingTimePes; | |
| 114 | } | |
| 115 | $queueWithWaitingTime->addEntity($nextWithAppointment); | |
| 116 | $nextWithAppointment = array_shift($listWithAppointment); | |
| 117 | } elseif ($nextNoAppointment) { | |
| 118 | $nextNoAppointment->waitingTimeEstimate = $waitingTimePes; | |
| 119 | $nextNoAppointment->waitingTimeOptimistic = $waitingTimeOpt; | |
| 120 | $queueWithWaitingTime->addEntity($nextNoAppointment); | |
| 121 | $nextNoAppointment = array_shift($listNoAppointment); | |
| 122 | } | |
| 123 | $optimisticTime += (--$workstationSkip > 0) ? 0 : $timeSlotOptimistic; | |
| 124 | $pessimisticTime += $timeSlotPessimistic; | |
| 125 | $currentTime += $timeSlot; | |
| 126 | $waitingTime = (int)ceil(($currentTime - $dateTime->getTimestamp()) / 60); | |
| 127 | $waitingTimeOpt = (int)floor(($optimisticTime - $dateTime->getTimestamp()) / 60); | |
| 128 | $waitingTimePes = (int)floor(($pessimisticTime - $dateTime->getTimestamp()) / 60); | |
| 129 | } | |
| 130 | $queueWithWaitingTime->addList($queueAppend); | |
| 131 | return $queueWithWaitingTime; | |
| 132 | } | |
| 133 | ||
| 134 | private function getSortPriority(\BO\Zmsentities\Queue $queue): int | |
| 135 | { | |
| 136 | $priority = self::DEFAULT_PRIORITY_WITHOUT_APPOINTMENT; | |
| 137 | if (empty($queue['priority']) && $queue['withAppointment']) { | |
| 138 | $priority = self::DEFAULT_PRIORITY_WITH_APPOINTMENT; | |
| 139 | } | |
| 140 | if (!empty($queue['priority'])) { | |
| 141 | $priority = (int) $queue['priority']; | |
| 142 | } | |
| 143 | ||
| 144 | return $priority; | |
| 145 | } | |
| 146 | ||
| 147 | public function withWaitingTime(\DateTimeInterface $dateTime): static | |
| 148 | { | |
| 149 | $queueList = $this; | |
| 150 | $timestamp = $dateTime->getTimestamp(); | |
| 151 | foreach ($queueList as $entity) { | |
| 152 | if ($timestamp > $entity->arrivalTime) { | |
| 153 | $entity->waitingTime = floor(($timestamp - $entity->arrivalTime) / 60); | |
| 154 | } | |
| 155 | } | |
| 156 | return $queueList; | |
| 157 | } | |
| 158 | ||
| 159 | public function withSortedArrival(): static | |
| 160 | { | |
| 161 | $queueList = $this; | |
| 162 | $queueList->uasort(function ($first, $second) { | |
| 163 | $firstPriority = $this->getSortPriority($first); | |
| 164 | $secondPriority = $this->getSortPriority($second); | |
| 165 | ||
| 166 | $firstSort = sprintf("%01d%011d%011d", $firstPriority, $first['arrivalTime'], $first['number']); | |
| 167 | $secondSort = sprintf("%01d%011d%011d", $secondPriority, $second['arrivalTime'], $second['number']); | |
| 168 | ||
| 169 | return strcmp($firstSort, $secondSort); | |
| 170 | }); | |
| 171 | return $queueList; | |
| 172 | } | |
| 173 | ||
| 174 | public function withSortedWaitingTime(): \BO\Zmsentities\Collection\Base | |
| 175 | { | |
| 176 | $queueList = $this; | |
| 177 | return $queueList->sortByCustomKey('waitingTimeEstimate'); | |
| 178 | } | |
| 179 | ||
| 180 | public function withAppointment(): self | |
| 181 | { | |
| 182 | $queueList = new self(); | |
| 183 | foreach ($this as $entity) { | |
| 184 | if ($entity->withAppointment) { | |
| 185 | $queueList->addEntity($entity); | |
| 186 | } | |
| 187 | } | |
| 188 | return $queueList; | |
| 189 | } | |
| 190 | ||
| 191 | public function withOutAppointment(): self | |
| 192 | { | |
| 193 | $queueList = new self(); | |
| 194 | foreach ($this as $entity) { | |
| 195 | if (! $entity->withAppointment) { | |
| 196 | $queueList->addEntity($entity); | |
| 197 | } | |
| 198 | } | |
| 199 | return $queueList; | |
| 200 | } | |
| 201 | ||
| 202 | public function getEstimatedWaitingTime(mixed $processTimeAverage, mixed $workstationCount, \DateTimeInterface $dateTime): array | |
| 203 | { | |
| 204 | $queueList = $this->withFakeWaitingnumber($dateTime); | |
| 205 | $queueList = $queueList | |
| 206 | ->withEstimatedWaitingTime($processTimeAverage, $workstationCount, $dateTime); | |
| 207 | $newEntity = $queueList->getFakeOrLastWaitingnumber(); | |
| 208 | if (!$newEntity instanceof \BO\Zmsentities\Queue) { | |
| 209 | return [ | |
| 210 | 'amountBefore' => 0, | |
| 211 | 'waitingTimeEstimate' => 0, | |
| 212 | ]; | |
| 213 | } | |
| 214 | $dataOfFackedEntity = array( | |
| 215 | 'amountBefore' => $queueList->getQueuePositionByNumber($newEntity->number), | |
| 216 | 'waitingTimeEstimate' => $newEntity->waitingTimeEstimate | |
| 217 | ); | |
| 218 | return $dataOfFackedEntity; | |
| 219 | } | |
| 220 | ||
| 221 | public function withFakeWaitingnumber(\DateTimeInterface $dateTime): static | |
| 222 | { | |
| 223 | $queueList = $this; | |
| 224 | $process = new \BO\Zmsentities\Process(['status' => 'deleted']); | |
| 225 | $entity = (new \BO\Zmsentities\Queue())->setProcess($process); | |
| 226 | $entity->number = self::FAKE_WAITINGNUMBER; | |
| 227 | $entity->status = 'fake'; | |
| 228 | $entity->withAppointment = false; | |
| 229 | $entity->destination = (string)$this->getProcessTimeAverage(); | |
| 230 | $entity->destinationHint = (string)$this->getWorkstationCount(); | |
| 231 | $entity->arrivalTime = $dateTime->getTimestamp(); | |
| 232 | $queueList->addEntity($entity); | |
| 233 | return $queueList; | |
| 234 | } | |
| 235 | ||
| 236 | public function getFakeOrLastWaitingnumber(): \BO\Zmsentities\Schema\Entity|false | |
| 237 | { | |
| 238 | $entity = $this->getQueueByNumber(self::FAKE_WAITINGNUMBER); | |
| 239 | if (!$entity) { | |
| 240 | $entity = $this->getLast(); | |
| 241 | } | |
| 242 | ||
| 243 | return $entity; | |
| 244 | } | |
| 245 | ||
| 246 | public function getQueueByNumber(int $number): \BO\Zmsentities\Queue|null | |
| 247 | { | |
| 248 | foreach ($this as $entity) { | |
| 249 | if ($entity->number == $number) { | |
| 250 | return $entity; | |
| 251 | } | |
| 252 | } | |
| 253 | return null; | |
| 254 | } | |
| 255 | ||
| 256 | public function getNextProcess(\DateTimeInterface $dateTime, mixed $exclude = null): mixed | |
| 257 | { | |
| 258 | if (is_array($exclude)) { | |
| 259 | $excludeNumbers = $exclude; | |
| 260 | } else { | |
| 261 | $excludeNumbers = explode(',', $exclude === null ? '' : (string) $exclude); | |
| 262 | } | |
| 263 | $queueList = clone $this; | |
| 264 | // sort by waiting time to get realistic next process | |
| 265 | $queueList = $queueList | |
| 266 | ->withStatus(['confirmed', 'queued']) | |
| 267 | ->withEstimatedWaitingTime(10, 1, $dateTime, false) | |
| 268 | ->getArrayCopy() | |
| 269 | ; | |
| 270 | $next = array_shift($queueList); | |
| 271 | $currentTime = $dateTime->getTimestamp(); | |
| 272 | while ($next) { | |
| 273 | if ( | |
| 274 | ! in_array($next->number, $excludeNumbers) && | |
| 275 | (0 == $next->lastCallTime || ($next->lastCallTime + (5 * 60)) <= $currentTime) | |
| 276 | ) { | |
| 277 | return $next->getProcess(); | |
| 278 | } | |
| 279 | $next = array_shift($queueList); | |
| 280 | } | |
| 281 | return null; | |
| 282 | } | |
| 283 | ||
| 284 | public function getQueuePositionByNumber(mixed $number): int|null | |
| 285 | { | |
| 286 | $list = array_values($this->getArrayCopy()); | |
| 287 | foreach ($list as $key => $entity) { | |
| 288 | if ($entity->number == $number) { | |
| 289 | return $key; | |
| 290 | } | |
| 291 | } | |
| 292 | return null; | |
| 293 | } | |
| 294 | ||
| 295 | public function getCountWithWaitingTime(?\DateTimeInterface $dateTime = null): self | |
| 296 | { | |
| 297 | $queueList = new self(); | |
| 298 | $timestamp = $dateTime ? $dateTime->getTimestamp() : null; | |
| 299 | ||
| 300 | foreach ($this as $entity) { | |
| 301 | if (! $entity->withAppointment) { | |
| 302 | $queueList->addEntity($entity); | |
| 303 | continue; | |
| 304 | } | |
| 305 | ||
| 306 | if ($timestamp !== null) { | |
| 307 | if ($entity->arrivalTime <= $timestamp) { | |
| 308 | $queueList->addEntity($entity); | |
| 309 | } | |
| 310 | ||
| 311 | continue; | |
| 312 | } | |
| 313 | ||
| 314 | if ($entity->waitingTime) { | |
| 315 | $queueList->addEntity($entity); | |
| 316 | } | |
| 317 | } | |
| 318 | return $queueList; | |
| 319 | } | |
| 320 | ||
| 321 | /** | |
| 322 | * @param array $statusList of possible strings in process.status | |
| 323 | */ | |
| 324 | public function withStatus(array $statusList): self | |
| 325 | { | |
| 326 | $queueList = new self(); | |
| 327 | foreach ($this as $entity) { | |
| 328 | if ($entity->toProperty()->status->isAvailable() && in_array($entity->status, $statusList)) { | |
| 329 | $queueList->addEntity($entity); | |
| 330 | } | |
| 331 | } | |
| 332 | return $queueList; | |
| 333 | } | |
| 334 | ||
| 335 | /** | |
| 336 | * @param array $statusList of excepted strings in process.status | |
| 337 | */ | |
| 338 | public function withoutStatus(array $statusList): self | |
| 339 | { | |
| 340 | $queueList = new self(); | |
| 341 | foreach ($this as $entity) { | |
| 342 | if ($entity->toProperty()->status->isAvailable() && ! in_array($entity->status, $statusList)) { | |
| 343 | $queueList->addEntity($entity); | |
| 344 | } | |
| 345 | } | |
| 346 | return $queueList; | |
| 347 | } | |
| 348 | ||
| 349 | public function withShortNameDestinationHint(\BO\Zmsentities\Cluster $cluster, \BO\Zmsentities\Scope $scope): self | |
| 350 | { | |
| 351 | $queueList = $this; | |
| 352 | $list = new self(); | |
| 353 | foreach ($queueList as $entity) { | |
| 354 | if ($cluster->shortNameEnabled && $scope->shortName) { | |
| 355 | $entity->destinationHint = $scope->shortName; | |
| 356 | } | |
| 357 | $list->addEntity($entity); | |
| 358 | } | |
| 359 | return $list; | |
| 360 | } | |
| 361 | ||
| 362 | public function toProcessList(): ProcessList | |
| 363 | { | |
| 364 | $processList = new ProcessList(); | |
| 365 | foreach ($this as $queue) { | |
| 366 | $process = $queue->getProcess(); | |
| 367 | if ($process) { | |
| 368 | $processList->addEntity($process); | |
| 369 | } | |
| 370 | } | |
| 371 | return $processList; | |
| 372 | } | |
| 373 | ||
| 374 | public function withoutDublicates(): self | |
| 375 | { | |
| 376 | $list = new self(); | |
| 377 | $exists = []; | |
| 378 | foreach ($this as $entity) { | |
| 379 | $key = "$entity->number-$entity->arrivalTime-$entity->withAppointment"; | |
| 380 | if (!isset($exists[$key])) { | |
| 381 | $list[] = $entity; | |
| 382 | $exists[$key] = true; | |
| 383 | } | |
| 384 | } | |
| 385 | return $list; // Cloning with this function | |
| 386 | } | |
| 387 | ||
| 388 | public function getWaitingNumberList(): array | |
| 389 | { | |
| 390 | $list = []; | |
| 391 | foreach ($this as $entity) { | |
| 392 | $list[] = $entity->number; | |
| 393 | } | |
| 394 | return $list; | |
| 395 | } | |
| 396 | ||
| 397 | public function getWaitingNumberListCsv(): string | |
| 398 | { | |
| 399 | return implode(',', $this->getWaitingNumberList()); | |
| 400 | } | |
| 401 | ||
| 402 | public function withSelectedProcessFirst(\BO\Zmsentities\Process $process): self | |
| 403 | { | |
| 404 | $queueList = $this; | |
| 405 | $list = new self(); | |
| 406 | $list->addEntity($process->queue); | |
| 407 | foreach ($queueList as $entity) { | |
| 408 | if ($entity->number != $process->queue->number) { | |
| 409 | $list->addEntity($entity); | |
| 410 | } | |
| 411 | } | |
| 412 | return $list; | |
| 413 | } | |
| 414 | ||
| 415 | public function sortByCallTime(string $order): self | |
| 416 | { | |
| 417 | $queueListArray = []; | |
| 418 | foreach ($this as $entity) { | |
| 419 | $queueListArray[] = $entity; | |
| 420 | } | |
| 421 | usort($queueListArray, function ($a, $b) use ($order) { | |
| 422 | if ($order === 'ascending') { | |
| 423 | return $a->callTime <=> $b->callTime; | |
| 424 | } elseif ($order === 'descending') { | |
| 425 | return $b->callTime <=> $a->callTime; | |
| 426 | } | |
| 427 | throw new \InvalidArgumentException("Invalid sort order: $order. Use 'ascending' or 'descending'."); | |
| 428 | }); | |
| 429 | return new self($queueListArray); | |
| 430 | } | |
| 431 | } |