Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.89% covered (warning)
86.89%
179 / 206
85.19% covered (warning)
85.19%
23 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueueList
86.89% covered (warning)
86.89%
179 / 206
85.19% covered (warning)
85.19%
23 / 27
101.27
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
85.96% covered (warning)
85.96%
49 / 57
0.00% covered (danger)
0.00%
0 / 1
16.71
 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%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 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'];
17
18    public const STATUS_APPEND = ['missed', 'parked'];
19
20    public const STATUS_CALLED = ['called', 'processing'];
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        $timeSlot = ($workstationCount) ? $processTimeAverage * 60 / $workstationCount : $processTimeAverage * 60;
85        $timeSlotOptimistic = $timeSlot * 0.8;
86        $timeSlotPessimistic = $timeSlot * 1.0;
87        $workstationSkip = ceil($workstationCount * 1.0);
88        $pessimisticTime += $timeSlot;
89        $waitingTimePes = (int)floor(($pessimisticTime - $dateTime->getTimestamp()) / 60);
90        while ($nextWithAppointment || $nextNoAppointment) {
91            if ($nextNoAppointment && (int) $nextNoAppointment->priority === 1) {
92                $nextNoAppointment->waitingTimeEstimate = $waitingTimePes;
93                $nextNoAppointment->waitingTimeOptimistic = $waitingTimeOpt;
94                $queueWithWaitingTime->addEntity($nextNoAppointment);
95                $nextNoAppointment = array_shift($listNoAppointment);
96            } elseif (
97                $nextNoAppointment
98                && $nextWithAppointment
99                && (int) $nextNoAppointment->priority === 2
100                && $nextNoAppointment->arrivalTime < $nextWithAppointment->arrivalTime
101            ) {
102                $nextNoAppointment->waitingTimeEstimate = $waitingTimePes;
103                $nextNoAppointment->waitingTimeOptimistic = $waitingTimeOpt;
104                $queueWithWaitingTime->addEntity($nextNoAppointment);
105                $nextNoAppointment = array_shift($listNoAppointment);
106            } elseif ($nextWithAppointment && $currentTime >= $nextWithAppointment->arrivalTime) {
107                $nextWithAppointment->waitingTimeEstimate = $waitingTime + 1;
108                $nextWithAppointment->waitingTimeOptimistic =
109                    floor(($nextWithAppointment->arrivalTime - $dateTime->getTimestamp()) / 60);
110                if ($optimisticTime >= $nextWithAppointment->arrivalTime) {
111                    $nextWithAppointment->waitingTimeOptimistic = $waitingTimeOpt;
112                    $nextWithAppointment->waitingTimeEstimate = $waitingTimePes;
113                }
114                $queueWithWaitingTime->addEntity($nextWithAppointment);
115                $nextWithAppointment = array_shift($listWithAppointment);
116            } elseif ($nextNoAppointment) {
117                $nextNoAppointment->waitingTimeEstimate = $waitingTimePes;
118                $nextNoAppointment->waitingTimeOptimistic = $waitingTimeOpt;
119                $queueWithWaitingTime->addEntity($nextNoAppointment);
120                $nextNoAppointment = array_shift($listNoAppointment);
121            }
122            $optimisticTime += (--$workstationSkip > 0) ? 0 : $timeSlotOptimistic;
123            $pessimisticTime += $timeSlotPessimistic;
124            $currentTime += $timeSlot;
125            $waitingTime = (int)ceil(($currentTime - $dateTime->getTimestamp()) / 60);
126            $waitingTimeOpt = (int)floor(($optimisticTime - $dateTime->getTimestamp()) / 60);
127            $waitingTimePes = (int)floor(($pessimisticTime - $dateTime->getTimestamp()) / 60);
128        }
129        $queueWithWaitingTime->addList($queueAppend);
130        return $queueWithWaitingTime;
131    }
132
133    private function getSortPriority($queue): int
134    {
135        $priority = self::DEFAULT_PRIORITY_WITHOUT_APPOINTMENT;
136        if (empty($queue['priority']) && $queue['withAppointment']) {
137            $priority = self::DEFAULT_PRIORITY_WITH_APPOINTMENT;
138        }
139        if (!empty($queue['priority'])) {
140            $priority = (int) $queue['priority'];
141        }
142
143        return $priority;
144    }
145
146    public function withWaitingTime(\DateTimeInterface $dateTime)
147    {
148        $queueList = $this;
149        $timestamp = $dateTime->getTimestamp();
150        foreach ($queueList as $entity) {
151            if ($timestamp > $entity->arrivalTime) {
152                $entity->waitingTime = floor(($timestamp - $entity->arrivalTime) / 60);
153            }
154        }
155        return $queueList;
156    }
157
158    public function withSortedArrival()
159    {
160        $queueList = $this;
161        $queueList->uasort(function ($first, $second) {
162            $firstPriority = $this->getSortPriority($first);
163            $secondPriority = $this->getSortPriority($second);
164
165            $firstSort = sprintf("%01d%011d%011d", $firstPriority, $first['arrivalTime'], $first['number']);
166            $secondSort = sprintf("%01d%011d%011d", $secondPriority, $second['arrivalTime'], $second['number']);
167
168            return strcmp($firstSort, $secondSort);
169        });
170        return $queueList;
171    }
172
173    public function withSortedWaitingTime()
174    {
175        $queueList = $this;
176        return $queueList->sortByCustomKey('waitingTimeEstimate');
177    }
178
179    public function withAppointment()
180    {
181        $queueList = new self();
182        foreach ($this as $entity) {
183            if ($entity->withAppointment) {
184                $queueList->addEntity($entity);
185            }
186        }
187        return $queueList;
188    }
189
190    public function withOutAppointment()
191    {
192        $queueList = new self();
193        foreach ($this as $entity) {
194            if (! $entity->withAppointment) {
195                $queueList->addEntity($entity);
196            }
197        }
198        return $queueList;
199    }
200
201    public function getEstimatedWaitingTime($processTimeAverage, $workstationCount, \DateTimeInterface $dateTime)
202    {
203        $queueList = $this->withFakeWaitingnumber($dateTime);
204        $queueList = $queueList
205          ->withEstimatedWaitingTime($processTimeAverage, $workstationCount, $dateTime);
206        $newEntity = $queueList->getFakeOrLastWaitingnumber();
207        $dataOfFackedEntity = array(
208            'amountBefore' => $queueList->getQueuePositionByNumber($newEntity->number),
209            'waitingTimeEstimate' => $newEntity->waitingTimeEstimate
210        );
211        return $dataOfFackedEntity;
212    }
213
214    public function withFakeWaitingnumber(\DateTimeInterface $dateTime)
215    {
216        $queueList = $this;
217        $process = new \BO\Zmsentities\Process(['status' => 'deleted']);
218        $entity = (new \BO\Zmsentities\Queue())->setProcess($process);
219        $entity->number = self::FAKE_WAITINGNUMBER;
220        $entity->status = 'fake';
221        $entity->withAppointment = false;
222        $entity->destination = (string)$this->getProcessTimeAverage();
223        $entity->destinationHint = (string)$this->getWorkstationCount();
224        $entity->arrivalTime = $dateTime->getTimestamp();
225        $queueList->addEntity($entity);
226        return $queueList;
227    }
228
229    public function getFakeOrLastWaitingnumber()
230    {
231        $entity = $this->getQueueByNumber(self::FAKE_WAITINGNUMBER);
232        if (!$entity) {
233            $entity = $this->getLast();
234        }
235
236        return $entity;
237    }
238
239    public function getQueueByNumber($number)
240    {
241        foreach ($this as $entity) {
242            if ($entity->number == $number) {
243                return $entity;
244            }
245        }
246        return null;
247    }
248
249    public function getNextProcess(\DateTimeInterface $dateTime, $exclude = null)
250    {
251        $excludeNumbers = explode(',', $exclude === null ? '' : $exclude);
252        $queueList = clone $this;
253        // sort by waiting time to get realistic next process
254        $queueList = $queueList
255            ->withStatus(['confirmed', 'queued'])
256            ->withEstimatedWaitingTime(10, 1, $dateTime, false)
257            ->getArrayCopy()
258            ;
259        $next = array_shift($queueList);
260        $currentTime = $dateTime->getTimestamp();
261        while ($next) {
262            if (
263                ! in_array($next->number, $excludeNumbers) &&
264                (0 == $next->lastCallTime || ($next->lastCallTime + (5 * 60)) <= $currentTime)
265            ) {
266                return $next->getProcess();
267            }
268            $next = array_shift($queueList);
269        }
270        return null;
271    }
272
273    public function getQueuePositionByNumber($number)
274    {
275        $list = array_values($this->getArrayCopy());
276        foreach ($list as $key => $entity) {
277            if ($entity->number == $number) {
278                return $key;
279            }
280        }
281        return null;
282    }
283
284    public function getCountWithWaitingTime()
285    {
286        $queueList = new self();
287        foreach ($this as $entity) {
288            if ($entity->waitingTime || ! $entity->withAppointment) {
289                $queueList->addEntity($entity);
290            }
291        }
292        return $queueList;
293    }
294
295    /**
296     * @param array $statusList of possible strings in process.status
297     *
298     */
299    public function withStatus(array $statusList)
300    {
301        $queueList = new self();
302        foreach ($this as $entity) {
303            if ($entity->toProperty()->status->isAvailable() && in_array($entity->status, $statusList)) {
304                $queueList->addEntity($entity);
305            }
306        }
307        return $queueList;
308    }
309
310    /**
311     * @param array $statusList of excepted strings in process.status
312     *
313     */
314    public function withoutStatus(array $statusList)
315    {
316        $queueList = new self();
317        foreach ($this as $entity) {
318            if ($entity->toProperty()->status->isAvailable() && ! in_array($entity->status, $statusList)) {
319                $queueList->addEntity($entity);
320            }
321        }
322        return $queueList;
323    }
324
325    public function withShortNameDestinationHint(\BO\Zmsentities\Cluster $cluster, \BO\Zmsentities\Scope $scope)
326    {
327        $queueList = $this;
328        $list = new self();
329        foreach ($queueList as $entity) {
330            if ($cluster->shortNameEnabled && $scope->shortName) {
331                $entity->destinationHint = $scope->shortName;
332            }
333            $list->addEntity($entity);
334        }
335        return $list;
336    }
337
338    public function toProcessList()
339    {
340        $processList = new ProcessList();
341        foreach ($this as $queue) {
342            $process = $queue->getProcess();
343            if ($process) {
344                $processList->addEntity($process);
345            }
346        }
347        return $processList;
348    }
349
350    public function withoutDublicates()
351    {
352        $list = new self();
353        $exists = [];
354        foreach ($this as $entity) {
355            $key = "$entity->number-$entity->arrivalTime-$entity->withAppointment";
356            if (!isset($exists[$key])) {
357                $list[] = $entity;
358                $exists[$key] = true;
359            }
360        }
361        return $list; // Cloning with this function
362    }
363
364    public function getWaitingNumberList()
365    {
366        $list = [];
367        foreach ($this as $entity) {
368            $list[] = $entity->number;
369        }
370        return $list;
371    }
372
373    public function getWaitingNumberListCsv()
374    {
375        return implode(',', $this->getWaitingNumberList());
376    }
377
378    public function withSelectedProcessFirst(\BO\Zmsentities\Process $process)
379    {
380        $queueList = $this;
381        $list = new self();
382        $list->addEntity($process->queue);
383        foreach ($queueList as $entity) {
384            if ($entity->number != $process->queue->number) {
385                $list->addEntity($entity);
386            }
387        }
388        return $list;
389    }
390
391    public function sortByCallTime(string $order)
392    {
393        $queueListArray = [];
394        foreach ($this as $entity) {
395            $queueListArray[] = $entity;
396        }
397        usort($queueListArray, function ($a, $b) use ($order) {
398            if ($order === 'ascending') {
399                return $a->callTime <=> $b->callTime;
400            } elseif ($order === 'descending') {
401                return $b->callTime <=> $a->callTime;
402            }
403            throw new InvalidArgumentException("Invalid sort order: $order. Use 'ascending' or 'descending'.");
404        });
405        return new self($queueListArray);
406    }
407}