Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.44% covered (warning)
87.44%
188 / 215
85.71% covered (warning)
85.71%
24 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueueList
87.44% covered (warning)
87.44%
188 / 215
85.71% covered (warning)
85.71%
24 / 28
103.34
0.00% covered (danger)
0.00%
0 / 1
 setWaitingTimePreferences
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setTransferedProcessList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getProcessTimeAverage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWorkstationCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 withEstimatedWaitingTime
86.21% covered (warning)
86.21%
50 / 58
0.00% covered (danger)
0.00%
0 / 1
16.67
 getSortPriority
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 withWaitingTime
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 withSortedArrival
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 withSortedWaitingTime
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 withAppointment
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 withOutAppointment
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getEstimatedWaitingTime
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 withFakeWaitingnumber
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getFakeOrLastWaitingnumber
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getQueueByNumber
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getNextProcess
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 getQueuePositionByNumber
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getCountWithWaitingTime
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 withStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 withoutStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 withShortNameDestinationHint
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 withPickupDestination
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 toProcessList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 withoutDublicates
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getWaitingNumberList
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getWaitingNumberListCsv
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withSelectedProcessFirst
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 sortByCallTime
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BO\Zmsentities\Collection;
4
5/**
6 * @SuppressWarnings(Complexity)
7 * @SuppressWarnings(PublicMethod)
8 *
9 */
10class QueueList extends Base implements \BO\Zmsentities\Helper\NoSanitize
11{
12    public const ENTITY_CLASS = '\BO\Zmsentities\Queue';
13
14    public const FAKE_WAITINGNUMBER = -1;
15
16    public const STATUS_IGNORE = ['called', 'processing', 'missed', 'parked', 'deleted', 'pickup'];
17
18    public const STATUS_APPEND = ['missed', 'parked', 'deleted'];
19
20    public const STATUS_CALLED = ['called', 'processing', 'pickup'];
21
22    public const STATUS_FAKE = ['fake'];
23
24    public const DEFAULT_PRIORITY_WITHOUT_APPOINTMENT = 3;
25
26    public const DEFAULT_PRIORITY_WITH_APPOINTMENT = 2;
27
28    protected $processTimeAverage;
29
30    protected $workstationCount;
31
32    protected $fromProcessList = false;
33
34    public function setWaitingTimePreferences($processTimeAverage, $workstationCount)
35    {
36        if ($processTimeAverage <= 0) {
37            throw new \Exception("QueueList::withEstimatedWaitingTime() requires processTimeAverage");
38        }
39        $this->processTimeAverage = $processTimeAverage;
40        $this->workstationCount = $workstationCount;
41        return $this;
42    }
43
44    public function setTransferedProcessList($bool = true)
45    {
46        $this->fromProcessList = $bool;
47        return $this;
48    }
49
50    public function getProcessTimeAverage()
51    {
52        return $this->processTimeAverage;
53    }
54
55    public function getWorkstationCount()
56    {
57        return $this->workstationCount ? $this->workstationCount : 1;
58    }
59
60    public function withEstimatedWaitingTime(
61        $processTimeAverage,
62        $workstationCount,
63        \DateTimeInterface $dateTime,
64        $createFake = true
65    ) {
66        $this->setWaitingTimePreferences($processTimeAverage, $workstationCount);
67        $queueFull = $this->withWaitingTime($dateTime);
68        $queueWithWaitingTime = $queueFull->withStatus(self::STATUS_CALLED);
69        $queueAppend = $queueFull->withStatus(self::STATUS_APPEND);
70        $queueFull = $queueFull->withoutStatus(self::STATUS_IGNORE);
71        if ($createFake) {
72            $queueFull = $queueFull->withFakeWaitingnumber($dateTime);
73        }
74        $listWithAppointment = $queueFull->withAppointment()->withSortedArrival()->getArrayCopy();
75        $listNoAppointment = $queueFull->withOutAppointment()->withSortedArrival()->getArrayCopy();
76        $nextWithAppointment = array_shift($listWithAppointment);
77        $nextNoAppointment = array_shift($listNoAppointment);
78        $currentTime = $dateTime->getTimestamp() + 120;
79        $optimisticTime = $dateTime->getTimestamp();
80        $pessimisticTime = $dateTime->getTimestamp();
81
82        $waitingTime = 0;
83        $waitingTimeOpt = 0;
84        $waitingTimePes = 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($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)
148    {
149        $queueList = clone $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()
160    {
161        $queueList = clone $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()
175    {
176        $queueList = clone $this;
177        return $queueList->sortByCustomKey('waitingTimeEstimate');
178    }
179
180    public function withAppointment()
181    {
182        $queueList = new self();
183        foreach ($this as $entity) {
184            if ($entity->withAppointment) {
185                $queueList->addEntity(clone $entity);
186            }
187        }
188        return $queueList;
189    }
190
191    public function withOutAppointment()
192    {
193        $queueList = new self();
194        foreach ($this as $entity) {
195            if (! $entity->withAppointment) {
196                $queueList->addEntity(clone $entity);
197            }
198        }
199        return $queueList;
200    }
201
202    public function getEstimatedWaitingTime($processTimeAverage, $workstationCount, \DateTimeInterface $dateTime)
203    {
204        $queueList = $this->withFakeWaitingnumber($dateTime);
205        $queueList = $queueList
206          ->withEstimatedWaitingTime($processTimeAverage, $workstationCount, $dateTime);
207        $newEntity = $queueList->getFakeOrLastWaitingnumber();
208        $dataOfFackedEntity = array(
209            'amountBefore' => $queueList->getQueuePositionByNumber($newEntity->number),
210            'waitingTimeEstimate' => $newEntity->waitingTimeEstimate
211        );
212        return $dataOfFackedEntity;
213    }
214
215    public function withFakeWaitingnumber(\DateTimeInterface $dateTime)
216    {
217        $queueList = clone $this;
218        $process = new \BO\Zmsentities\Process(['status' => 'deleted']);
219        $entity = (new \BO\Zmsentities\Queue())->setProcess($process);
220        $entity->number = self::FAKE_WAITINGNUMBER;
221        $entity->status = 'fake';
222        $entity->withAppointment = false;
223        $entity->destination = (string)$this->getProcessTimeAverage();
224        $entity->destinationHint = (string)$this->getWorkstationCount();
225        $entity->arrivalTime = $dateTime->getTimestamp();
226        $queueList->addEntity($entity);
227        return $queueList;
228    }
229
230    public function getFakeOrLastWaitingnumber()
231    {
232        $entity = $this->getQueueByNumber(self::FAKE_WAITINGNUMBER);
233        if (!$entity) {
234            $entity = $this->getLast();
235        }
236
237        return $entity;
238    }
239
240    public function getQueueByNumber($number)
241    {
242        foreach ($this as $entity) {
243            if ($entity->number == $number) {
244                return $entity;
245            }
246        }
247        return null;
248    }
249
250    public function getNextProcess(\DateTimeInterface $dateTime, $exclude = null)
251    {
252        $excludeNumbers = explode(',', $exclude === null ? '' : $exclude);
253        $queueList = clone $this;
254        // sort by waiting time to get realistic next process
255        $queueList = $queueList
256            ->withStatus(['confirmed', 'queued'])
257            ->withEstimatedWaitingTime(10, 1, $dateTime, false)
258            ->getArrayCopy()
259            ;
260        $next = array_shift($queueList);
261        $currentTime = $dateTime->getTimestamp();
262        while ($next) {
263            if (
264                ! in_array($next->number, $excludeNumbers) &&
265                (0 == $next->lastCallTime || ($next->lastCallTime + (5 * 60)) <= $currentTime)
266            ) {
267                return $next->getProcess();
268            }
269            $next = array_shift($queueList);
270        }
271        return null;
272    }
273
274    public function getQueuePositionByNumber($number)
275    {
276        $list = array_values($this->getArrayCopy());
277        foreach ($list as $key => $entity) {
278            if ($entity->number == $number) {
279                return $key;
280            }
281        }
282        return null;
283    }
284
285    public function getCountWithWaitingTime()
286    {
287        $queueList = new self();
288        foreach ($this as $entity) {
289            if ($entity->waitingTime || ! $entity->withAppointment) {
290                $queueList->addEntity(clone $entity);
291            }
292        }
293        return $queueList;
294    }
295
296    /**
297     * @param array $statusList of possible strings in process.status
298     *
299     */
300    public function withStatus(array $statusList)
301    {
302        $queueList = new self();
303        foreach ($this as $entity) {
304            if ($entity->toProperty()->status->isAvailable() && in_array($entity->status, $statusList)) {
305                $queueList->addEntity(clone $entity);
306            }
307        }
308        return $queueList;
309    }
310
311    /**
312     * @param array $statusList of excepted strings in process.status
313     *
314     */
315    public function withoutStatus(array $statusList)
316    {
317        $queueList = new self();
318        foreach ($this as $entity) {
319            if ($entity->toProperty()->status->isAvailable() && ! in_array($entity->status, $statusList)) {
320                $queueList->addEntity(clone $entity);
321            }
322        }
323        return $queueList;
324    }
325
326    public function withShortNameDestinationHint(\BO\Zmsentities\Cluster $cluster, \BO\Zmsentities\Scope $scope)
327    {
328        $queueList = clone $this;
329        $list = new self();
330        foreach ($queueList as $entity) {
331            if ($cluster->shortNameEnabled && $scope->shortName) {
332                $entity->destinationHint = $scope->shortName;
333            }
334            $list->addEntity($entity);
335        }
336        $listWithPickups = $list->withPickupDestination($scope);
337        return $listWithPickups;
338    }
339
340    public function withPickupDestination(\BO\Zmsentities\Scope $scope)
341    {
342        $queueList = clone $this;
343        $list = new self();
344        foreach ($queueList as $entity) {
345            if (! $entity->toProperty()->destination->get()) {
346                $entity->destination = $scope->toProperty()->preferences->pickup->alternateName->get();
347            }
348            $list->addEntity($entity);
349        }
350        return $list;
351    }
352
353    public function toProcessList()
354    {
355        $processList = new ProcessList();
356        foreach ($this as $queue) {
357            $process = $queue->getProcess();
358            if ($process) {
359                $processList->addEntity($process);
360            }
361        }
362        return $processList;
363    }
364
365    public function withoutDublicates()
366    {
367        $list = new self();
368        $exists = [];
369        foreach ($this as $entity) {
370            $key = "$entity->number-$entity->arrivalTime-$entity->withAppointment";
371            if (!isset($exists[$key])) {
372                $list[] = $entity;
373                $exists[$key] = true;
374            }
375        }
376        return $list; // Cloning with this function
377    }
378
379    public function getWaitingNumberList()
380    {
381        $list = [];
382        foreach ($this as $entity) {
383            $list[] = $entity->number;
384        }
385        return $list;
386    }
387
388    public function getWaitingNumberListCsv()
389    {
390        return implode(',', $this->getWaitingNumberList());
391    }
392
393    public function withSelectedProcessFirst(\BO\Zmsentities\Process $process)
394    {
395        $queueList = clone $this;
396        $list = new self();
397        $list->addEntity($process->queue);
398        foreach ($queueList as $entity) {
399            if ($entity->number != $process->queue->number) {
400                $list->addEntity($entity);
401            }
402        }
403        return $list;
404    }
405
406    public function sortByCallTime(string $order)
407    {
408        $queueListArray = [];
409        foreach ($this as $entity) {
410            $queueListArray[] = $entity;
411        }
412        usort($queueListArray, function ($a, $b) use ($order) {
413            if ($order === 'ascending') {
414                return $a->callTime <=> $b->callTime;
415            } elseif ($order === 'descending') {
416                return $b->callTime <=> $a->callTime;
417            }
418            throw new InvalidArgumentException("Invalid sort order: $order. Use 'ascending' or 'descending'.");
419        });
420        return new self($queueListArray);
421    }
422}