Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.68% covered (success)
99.68%
307 / 308
96.77% covered (success)
96.77%
30 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Availability
99.68% covered (success)
99.68%
307 / 308
96.77% covered (success)
96.77%
30 / 31
124
0.00% covered (danger)
0.00%
0 / 1
 getDefaults
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
1
 hasDate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hasBookableDates
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 isOpenedOnDate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 isOpened
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 hasWeekDay
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 hasAppointment
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 hasTime
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getAvailableSecondsPerDay
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasDay
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hasDayOff
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 hasWeek
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
10
 getStartDateTime
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getEndDateTime
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getDuration
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getBookableStart
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getBookableEnd
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 isBookable
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 getSlotList
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 getSlotTimeInMinutes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasDateBetween
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getConflict
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 isMatchOf
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
15
 hasSharedWeekdayWith
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
9
 getTimeOverlaps
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
12
 withCalculatedSlots
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 withScope
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 offsetSet
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isNewerThan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withLessData
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3namespace BO\Zmsentities;
4
5/**
6 * @SuppressWarnings(Complexity)
7 * @SuppressWarnings(Coupling)
8 * @SuppressWarnings(PublicMethod)
9 *
10 */
11class Availability extends Schema\Entity
12{
13    const PRIMARY = 'id';
14
15    public static $schema = "availability.json";
16
17    /**
18     * @var array $weekday english localized weekdays to avoid problems with setlocale()
19     */
20    protected static $weekdayNameList = [
21        'sunday',
22        'monday',
23        'tuesday',
24        'wednesday',
25        'thursday',
26        'friday',
27        'saturday'
28    ];
29
30    /**
31     * Performance costs for modifying time are high, cache the calculated value
32     * @var \DateTimeImmutable $startTimeCache
33     */
34    protected $startTimeCache;
35
36    /**
37     * Performance costs for modifying time are high, cache the calculated value
38     * @var \DateTimeImmutable $endTimeCache
39     */
40    protected $endTimeCache;
41
42    /**
43     * Set Default values
44     */
45    public function getDefaults()
46    {
47        return [
48            'id' => 0,
49            'weekday' => array_fill_keys(self::$weekdayNameList, 0),
50            'repeat' => [
51                'afterWeeks' => 1,
52                'weekOfMonth' => 0,
53            ],
54            'bookable' => [
55                'startInDays' => 1,
56                'endInDays' => 60,
57            ],
58            'workstationCount' => [
59                'public' => 0,
60                'callcenter' => 0,
61                'intern' => 0,
62            ],
63            'lastChange' => 0,
64            'multipleSlotsAllowed' => true,
65            'slotTimeInMinutes' => 10,
66            'startDate' => 0,
67            'endDate' => 0,
68            'startTime' => "0:00",
69            'endTime' => "23:59",
70            'type' => 'appointment'
71        ];
72    }
73
74    /**
75     * Check, if the dateTime contains a day given by the settings
76     * ATTENTION: Time critical function, keep highly optimized
77     * Compared to isOpened() the Booking time is checked too
78     *
79     * @param \DateTimeInterface $dateTime
80     *
81     * @return Bool
82     */
83    public function hasDate(\DateTimeInterface $dateTime, \DateTimeInterface $now)
84    {
85        $dateTime = Helper\DateTime::create($dateTime);
86        if (
87            !$this->isOpenedOnDate($dateTime)
88            || !$this->isBookable($dateTime, $now)
89        ) {
90            // Out of date range
91            return false;
92        }
93        return true;
94    }
95
96    public function hasBookableDates(\DateTimeInterface $now)
97    {
98        if ($this->workstationCount['intern'] <= 0) {
99            return false;
100        }
101        if ($this->getEndDateTime()->getTimestamp() < $now->getTimestamp()) {
102            return false;
103        }
104        $stopDate = $this->getBookableEnd($now);
105        if ($this->getStartDateTime()->getTimestamp() > $stopDate->getTimestamp()) {
106            return false;
107        }
108        return $this->hasDateBetween($this->getBookableStart($now), $this->getBookableEnd($now), $now);
109    }
110
111    /**
112     * Check, if the dateTime contains a day
113     * ATTENTION: Time critical function, keep highly optimized
114     *
115     * @param \DateTimeInterface $dateTime
116     * @param String $type of "openinghours", "appointment" or false to ignore type
117     *
118     * @return Bool
119     */
120    public function isOpenedOnDate(\DateTimeInterface $dateTime, $type = false)
121    {
122        $dateTime = Helper\DateTime::create($dateTime);
123        if (
124            !$this->hasWeekDay($dateTime)
125            || ($type !== false && $this->type != $type)
126            || !$this->hasDay($dateTime)
127            || !$this->hasWeek($dateTime)
128            || ($this->getDuration() > 2 && $this->hasDayOff($dateTime))
129        ) {
130            // Out of date range
131            return false;
132        }
133        return true;
134    }
135
136    /**
137     * Check if date and time is in availability
138     * Compared to hasDate() the time of the day is checked, but not booking time
139     *
140     * @param \DateTimeInterface $dateTime
141     * @param String $type of "openinghours", "appointment" or false to ignore type
142     *
143     */
144    public function isOpened(\DateTimeInterface $dateTime, $type = false)
145    {
146        return (!$this->isOpenedOnDate($dateTime, $type) || !$this->hasTime($dateTime)) ? false : true;
147    }
148
149    public function hasWeekDay(\DateTimeInterface $dateTime)
150    {
151        $weekDayName = self::$weekdayNameList[$dateTime->format('w')];
152        if (!$this['weekday'][$weekDayName]) {
153            // Wrong weekday
154            return false;
155        }
156        return true;
157    }
158
159    public function hasAppointment(Appointment $appointment)
160    {
161        $dateTime = $appointment->toDateTime();
162        $isOpenedStart = $this->isOpened($dateTime, false);
163        $duration = $this->slotTimeInMinutes * $appointment->slotCount;
164        $endTime = $dateTime->modify("+" . $duration . "minutes")
165            ->modify("-1 second"); // To allow the last slot for an appointment
166        $isOpenedEnd = $this->isOpened($endTime, false);
167        return ($isOpenedStart && $isOpenedEnd);
168    }
169
170    /**
171     * Check, if the dateTime is a time covered by availability
172     *
173     * @param \DateTimeInterface $dateTime
174     *
175     * @return Bool
176     */
177    public function hasTime(\DateTimeInterface $dateTime)
178    {
179        $start = $this->getStartDateTime()->getSecondsOfDay();
180        $end = $this->getEndDateTime()->getSecondsOfDay();
181        $compare = Helper\DateTime::create($dateTime)->getSecondsOfDay();
182        if ($start > $compare || $end <= $compare) {
183            // Out of time range
184            return false;
185        }
186        return true;
187    }
188
189    public function getAvailableSecondsPerDay($type = "intern")
190    {
191        $start = $this->getStartDateTime()->getSecondsOfDay();
192        $end = $this->getEndDateTime()->getSecondsOfDay();
193        return ($end - $start) * $this->workstationCount[$type];
194    }
195
196    /**
197     * Check, if the dateTime is a day covered by availability
198     *
199     * @param \DateTimeInterface $dateTime
200     *
201     * @return Bool
202     */
203    public function hasDay(\DateTimeInterface $dateTime)
204    {
205        $start = $this->getStartDateTime()->modify('0:00:00');
206        $end = $this->getEndDateTime()->modify('23:59:59');
207        if ($dateTime->getTimestamp() < $start->getTimestamp() || $dateTime->getTimestamp() > $end->getTimestamp()) {
208            // Out of date range
209            return false;
210        }
211        return true;
212    }
213
214    /**
215     * Check, if the dateTime is a dayoff date
216     *
217     * @param \DateTimeInterface $dateTime
218     *
219     * @return Bool
220     */
221    public function hasDayOff(\DateTimeInterface $dateTime)
222    {
223        if (isset($this['scope']['dayoff'])) {
224            $timeStamp = $dateTime->format('Y-m-d');
225            foreach ($this['scope']['dayoff'] as $dayOff) {
226                if (date('Y-m-d', $dayOff['date']) == $timeStamp) {
227                    return true;
228                }
229            }
230        } else {
231            throw new Exception\DayoffMissing();
232        }
233        return false;
234    }
235
236    /**
237     * Check, if the dateTime contains a week given by the week repetition settings
238     *
239     * @param \DateTimeInterface $dateTime
240     *
241     * @return Bool
242     */
243    public function hasWeek(\DateTimeInterface $dateTime)
244    {
245        $dateTime = Helper\DateTime::create($dateTime);
246        $start = $this->getStartDateTime();
247        $monday = "monday this week";
248        if (
249            $this['repeat']['afterWeeks']
250            && ($this['repeat']['afterWeeks'] == 1
251                || 0 ===
252                    $dateTime->modify($monday)->diff($start->modify($monday))->days
253                 % ($this['repeat']['afterWeeks'] * 7)
254            )
255        ) {
256            return true;
257        }
258        if (
259            $this['repeat']['weekOfMonth']
260            && (
261                $dateTime->isWeekOfMonth($this['repeat']['weekOfMonth'])
262                // On a value of 5, always take the last week
263                || ($this['repeat']['weekOfMonth'] >= 5 && $dateTime->isLastWeekOfMonth())
264            )
265        ) {
266            return true;
267        }
268        if (!$this['repeat']['weekOfMonth'] && !$this['repeat']['afterWeeks']) {
269            return true;
270        }
271        return false;
272    }
273
274    /**
275     * Get DateTimeInterface for start time of availability
276     *
277     * @return \DateTimeInterface
278     */
279    public function getStartDateTime()
280    {
281        if (!$this->startTimeCache) {
282            $this->startTimeCache = Helper\DateTime::create()
283                ->setTimestamp($this['startDate'])
284                ->modify('today ' .  $this['startTime']);
285        }
286        return $this->startTimeCache;
287    }
288
289    /**
290     * Get DateTimeInterface for end time of availability
291     *
292     * @return \DateTimeInterface
293     */
294    public function getEndDateTime()
295    {
296        if (!$this->endTimeCache) {
297            $this->endTimeCache = Helper\DateTime::create()
298                ->setTimestamp($this['endDate'])
299                ->modify('today ' .  $this['endTime']);
300        }
301        return $this->endTimeCache;
302    }
303
304    /**
305     * Get duration of availability
306     *
307     * @return integer
308     */
309    public function getDuration()
310    {
311        $startTime = $this->getStartDateTime();
312        $endTime = $this->getEndDateTime();
313        return (int)$endTime->diff($startTime)->format("%a");
314    }
315
316    /**
317     * Get DateTimeInterface for start booking time of availability
318     *
319     * @param \DateTimeInterface $now relative time to compare booking settings
320     *
321     * @return \DateTimeInterface
322     */
323    public function getBookableStart(\DateTimeInterface $now)
324    {
325        $now = Helper\DateTime::create($now);
326        $availabilityStart = Helper\Property::create($this)->bookable->startInDays->get();
327        $time = $this->getStartDateTime()->format('H:i:s');
328        if (null !== $availabilityStart) {
329            return $now->modify('+' . $availabilityStart . 'days')->modify($time);
330        }
331        $scopeStart = Helper\Property::create($this)->scope->preferences->appointment->startInDaysDefault->get();
332        if (null !== $scopeStart) {
333            return $now->modify('+' . $scopeStart . 'days')->modify($time);
334        }
335        throw new \BO\Zmsentities\Exception\ProcessBookableFailed(
336            "Undefined start time for booking, try to set the scope properly"
337        );
338    }
339
340    /**
341     * Get DateTimeInterface for end booking time of availability
342     *
343     * @param \DateTimeInterface $now relative time to compare booking settings
344     *
345     * @return \DateTimeInterface
346     */
347    public function getBookableEnd(\DateTimeInterface $now)
348    {
349        $now = Helper\DateTime::create($now);
350        $availabilityEnd = Helper\Property::create($this)->bookable->endInDays->get();
351        $time = $this->getEndDateTime()->format('H:i:s');
352        if (null !== $availabilityEnd) {
353            return $now->modify('+' . $availabilityEnd . 'days')->modify($time);
354        }
355        $scopeEnd = Helper\Property::create($this)->scope->preferences->appointment->endInDaysDefault->get();
356        if (null !== $scopeEnd) {
357            return $now->modify('+' . $scopeEnd . 'days')->modify($time);
358        }
359        throw new \BO\Zmsentities\Exception\ProcessBookableFailed(
360            "Undefined end time for booking, try to set the scope properly"
361        );
362    }
363
364    /**
365     * Check, if the dateTime contains is within the bookable range (usually for public access)
366     * The current time is used to compare the start Time of the availability
367     *
368     * @param \DateTimeInterface $dateTime
369     * @param \DateTimeInterface $now relative time to compare booking settings
370     *
371     * @return Bool
372     */
373    public function isBookable(\DateTimeInterface $bookableDate, \DateTimeInterface $now)
374    {
375        if (!$this->hasDay($bookableDate)) {
376            return false;
377        }
378        $bookableCurrentTime = $bookableDate->modify($now->format('H:i:s'));
379        Helper\DateTime::create($bookableDate)->getTimestamp() + Helper\DateTime::create($now)->getSecondsOfDay();
380        $startDate = $this->getBookableStart($now)->modify('00:00:00');
381
382        if ($bookableCurrentTime->getTimestamp() < $startDate->getTimestamp()) {
383            //error_log("START " . $bookableCurrentTime->format('c').'<'.$startDate->format('c'). " " . $this);
384            return false;
385        }
386        $endDate = $this->getBookableEnd($now)->modify('23:59:59');
387        if ($bookableCurrentTime->getTimestamp() > $endDate->getTimestamp()) {
388            //error_log("END " . $bookableCurrentTime->format('c').'>'.$endDate->format('c'). " " . $this);
389            return false;
390        }
391        if (
392            $bookableDate->format('Y-m-d') == $endDate->format('Y-m-d')
393            && $now->format('Y-m-d') != $this->getEndDateTime()->format('Y-m-d')
394        ) {
395            // Avoid releasing all appointments on midnight, allow smaller contingents distributed over the day
396            $delayedStart = $this->getBookableEnd($now)->modify($this->getStartDateTime()->format('H:i:s'));
397            if ($bookableCurrentTime->getTimestamp() < $delayedStart->getTimestamp()) {
398                //error_log(
399                //    sprintf("DELAY %s<%s", $bookableCurrentTime->format('c'), $delayedStart->format('c'))
400                //    ." $this"
401                //);
402                return false;
403            }
404        }
405        return true;
406    }
407
408    /**
409     * Creates a list of slots available on a valid day
410     *
411     * @return Array of arrays with the keys time, public, callcenter, intern
412     */
413    public function getSlotList()
414    {
415        $startTime = Helper\DateTime::create($this['startTime']);
416        $stopTime = Helper\DateTime::create($this['endTime']);
417        $slotList = new Collection\SlotList();
418        $slotInstance = new Slot($this['workstationCount']);
419        if ($this['slotTimeInMinutes'] > 0) {
420            do {
421                $slot = clone $slotInstance;
422                $slot->setTime($startTime);
423                $slotList[] = $slot;
424                $startTime = $startTime->modify('+' . $this['slotTimeInMinutes'] . 'minute');
425                // Only add a slot, if at least a minute is left, otherwise do not ("<" instead "<=")
426            } while ($startTime->getTimestamp() < $stopTime->getTimestamp());
427        }
428        return $slotList;
429    }
430
431    public function getSlotTimeInMinutes()
432    {
433        return $this['slotTimeInMinutes'];
434    }
435
436
437    /**
438     * Check, if a day between two dates is included
439     *
440     * @return Array of arrays with the keys time, public, callcenter, intern
441     */
442    public function hasDateBetween(\DateTimeInterface $startTime, \DateTimeInterface $stopTime, \DateTimeInterface $now)
443    {
444        if ($startTime->getTimestamp() < $now->getTimestamp()) {
445            $startTime = $now;
446        }
447        if ($stopTime->getTimestamp() < $now->getTimestamp()) {
448            return false;
449        }
450        do {
451            if ($this->hasDate($startTime, $now)) {
452                return true;
453            }
454            $startTime = $startTime->modify('+1 day');
455        } while ($startTime->getTimestamp() <= $stopTime->getTimestamp());
456        return false;
457    }
458
459    /**
460     * Get problems on configuration of this availability
461     *
462     * @return Collection\ProcessList with processes in status "conflict"
463     */
464    public function getConflict()
465    {
466        $start = $this->getStartDateTime()->getSecondsOfDay();
467        $end = $this->getEndDateTime()->getSecondsOfDay();
468        $minutesPerDay = floor(($end - $start) / 60);
469        if ($minutesPerDay % $this->slotTimeInMinutes > 0) {
470            $conflict = new Process();
471            $conflict->status = 'conflict';
472            $appointment = $conflict->getFirstAppointment();
473            $appointment->availability = $this;
474            $appointment->date = $this->getStartDateTime()->getTimestamp();
475            $conflict->amendment =
476                "Der eingestellte Zeitschlitz von {$this->slotTimeInMinutes} Minuten"
477                . " sollte in die eingestellte Uhrzeit passen.";
478            return $conflict;
479        }
480        return false;
481    }
482
483    /**
484     * Check of a different availability has the same opening configuration
485     *
486     */
487    public function isMatchOf(Availability $availability)
488    {
489        return ($this->type != $availability->type
490            || $this->startTime != $availability->startTime
491            || $this->endTime != $availability->endTime
492            || $this->startDate != $availability->startDate
493            || $this->endDate != $availability->endDate
494            || $this->repeat['afterWeeks'] != $availability->repeat['afterWeeks']
495            || $this->repeat['weekOfMonth'] != $availability->repeat['weekOfMonth']
496            || (bool)$this->weekday['monday'] != (bool)$availability->weekday['monday']
497            || (bool)$this->weekday['tuesday'] != (bool)$availability->weekday['tuesday']
498            || (bool)$this->weekday['wednesday'] != (bool)$availability->weekday['wednesday']
499            || (bool)$this->weekday['thursday'] != (bool)$availability->weekday['thursday']
500            || (bool)$this->weekday['friday'] != (bool)$availability->weekday['friday']
501            || (bool)$this->weekday['saturday'] != (bool)$availability->weekday['saturday']
502            || (bool)$this->weekday['sunday'] != (bool)$availability->weekday['sunday']
503        ) ? false : true;
504    }
505
506    public function hasSharedWeekdayWith(Availability $availability)
507    {
508        return ($this->type == $availability->type
509            && (bool)$this->weekday['monday'] != (bool)$availability->weekday['monday']
510            && (bool)$this->weekday['tuesday'] != (bool)$availability->weekday['tuesday']
511            && (bool)$this->weekday['wednesday'] != (bool)$availability->weekday['wednesday']
512            && (bool)$this->weekday['thursday'] != (bool)$availability->weekday['thursday']
513            && (bool)$this->weekday['friday'] != (bool)$availability->weekday['friday']
514            && (bool)$this->weekday['saturday'] != (bool)$availability->weekday['saturday']
515            && (bool)$this->weekday['sunday'] != (bool)$availability->weekday['sunday']
516        ) ? false : true;
517    }
518
519    /**
520     * Get overlaps on daytime
521     * This functions does not check, if two availabilities are openend on the same day!
522     *
523     * @param Availability $availability for comparision
524     *
525     * @return Collection\ProcessList with processes in status "conflict"
526     *
527     *
528     */
529
530    /*
531    1
532    Case 01:  |-----|
533              |-----|
534                 2
535
536                 1
537    Case 02:  |-----|
538                 |-----|
539                    2
540
541                    1
542    Case 03:     |-----|
543              |-----|
544                 2
545
546                   1
547    Case 04:  |---------|
548                |-----|
549                   2
550
551                   1
552    Case 05:    |-----|
553              |---------|
554                   2
555
556                 1
557    Case 06:  |-----|
558                      |-----|
559                         2
560
561                         1
562    Case 07:          |-----|
563              |-----|
564                 2
565
566                 1
567    Case 08:  |-----|
568                    |-----|
569                       2
570
571                       1
572    Case 09:        |-----|
573              |-----|
574                 2
575
576                 1
577    Case 10:     |
578              |-----|
579                 2
580
581                 1
582    Case 11:  |-----|
583                 |
584                 2
585
586              1
587    Case 12:  |
588              |-----|
589                 2
590
591                    1
592    Case 13:        |
593              |-----|
594                 2
595
596                 1
597    Case 14:  |-----|
598              |
599              2
600
601                 1
602    Case 15:  |-----|
603                    |
604                    2
605
606              1
607    Case 16:  |
608              |
609              2
610
611            |                         |    Operlap    |     Overlap
612      Case  |         Example         | Open Interval | Closed Interval
613    --------|-------------------------|---------------|-----------------
614    Case 01 | 09:00-11:00 09:00-11:00 |      Yes      |        Yes
615    Case 02 | 09:00-11:00 10:00-12:00 |      Yes      |        Yes
616    Case 03 | 10:00-12:00 09:00-11:00 |      Yes      |        Yes
617    Case 04 | 09:00-12:00 10:00-11:00 |      Yes      |        Yes
618    Case 05 | 10:00-11:00 09:00-12:00 |      Yes      |        Yes
619    Case 06 | 09:00-10:00 11:00-12:00 |      No       |        No
620    Case 07 | 11:00-12:00 09:00-10:00 |      No       |        No
621    Case 08 | 09:00-10:00 10:00-11:00 |      No       |        Yes
622    Case 09 | 10:00-11:00 09:00-10:00 |      No       |        Yes
623    Case 10 | 10:00-10:00 09:00-11:00 |      Yes      |        Yes
624    Case 11 | 09:00-11:00 10:00-10:00 |      Yes      |        Yes
625    Case 12 | 09:00-09:00 09:00-10:00 |      No       |        Yes
626    Case 13 | 10:00-10:00 09:00-10:00 |      No       |        Yes
627    Case 14 | 09:00-10:00 09:00-09:00 |      No       |        Yes
628    Case 15 | 09:00-10:00 10:00-10:00 |      No       |        Yes
629    Case 16 | 09:00-09:00 09:00-09:00 |      No       |        Yes
630    */
631
632    public function getTimeOverlaps(Availability $availability, \DateTimeInterface $currentDate)
633    {
634        $processList = new Collection\ProcessList();
635        if (
636            $availability->id != $this->id
637            && $availability->type == $this->type
638            && $this->hasSharedWeekdayWith($availability)
639        ) {
640            $processTemplate = new Process();
641            $processTemplate->amendment = "Zwei Ã–ffnungszeiten Ã¼berschneiden sich.";
642            $processTemplate->status = 'conflict';
643            $appointment = $processTemplate->getFirstAppointment();
644            $appointment->availability = $this;
645            $appointment->date = $this->getStartDateTime()->getTimestamp();
646            $thisStart = $this->getStartDateTime()->getSecondsOfDay();
647            $thisEnd = $this->getEndDateTime()->getSecondsOfDay();
648            $availabilityStart = $availability->getStartDateTime()->getSecondsOfDay();
649            $availabilityEnd = $availability->getEndDateTime()->getSecondsOfDay();
650
651            $isEqual = ($availabilityStart == $thisStart && $availabilityEnd == $thisEnd);
652
653            if ($availabilityStart < $thisEnd && $thisStart < $availabilityEnd && ! $isEqual) {
654                $process = clone $processTemplate;
655                $process->getFirstAppointment()->date = $this
656                    ->getStartDateTime()
657                    ->modify($currentDate->format("Y-m-d"))
658                    ->getTimestamp();
659                $processList->addEntity($process);
660            } elseif ($thisEnd < $availabilityStart && $availabilityEnd < $thisStart && ! $isEqual) {
661                $process = clone $processTemplate;
662                $process->getFirstAppointment()->date = $availability
663                    ->getStartDateTime()
664                    ->modify($currentDate->format("Y-m-d"))
665                    ->getTimestamp();
666                $processList->addEntity($process);
667            } elseif ($isEqual) {
668                $process = clone $processTemplate;
669                $process->amendment = "Zwei Ã–ffnungszeiten sind gleich.";
670                $process->getFirstAppointment()->date = $availability
671                    ->getStartDateTime()
672                    ->modify($currentDate->format("Y-m-d"))
673                    ->getTimestamp();
674                $processList->addEntity($process);
675            }
676        }
677        return $processList;
678    }
679
680    /**
681     * Update workstationCount to number of calculated appointments
682     *
683     * @return self cloned
684     */
685    public function withCalculatedSlots()
686    {
687        $availability = clone $this;
688        $startTime = Helper\DateTime::create($this['startTime']);
689        $stopTime = Helper\DateTime::create($this['endTime']);
690        $openingSeconds = $stopTime->getTimestamp() - $startTime->getTimestamp();
691        $openingMinutes = floor($openingSeconds / 60);
692        $slices = 0;
693        if ($this['slotTimeInMinutes'] > 0) {
694            $slices = floor($openingMinutes / $this['slotTimeInMinutes']);
695        }
696        $slot = new Slot([
697            'type' => Slot::FREE,
698            'intern' => $this['workstationCount']['intern'] * $slices,
699            'callcenter' => $this['workstationCount']['callcenter'] * $slices,
700            'public' => $this['workstationCount']['public'] * $slices,
701        ]);
702        $availability['workstationCount'] = $slot;
703        return $availability;
704    }
705
706    public function withScope(\BO\Zmsentities\Scope $scope)
707    {
708        $availability = clone $this;
709        $availability->scope = $scope;
710        return $availability;
711    }
712
713    public function __toString()
714    {
715        $info = "Availability." . $this['type'] . " #" . $this['id'];
716        $info .= " starting " . $this->startDate . $this->getStartDateTime()->format(' Y-m-d');
717        $info .= "||now+" . $this['bookable']['startInDays'] . " ";
718        $info .= " until " . $this->getEndDateTime()->format('Y-m-d');
719        $info .= "||now+" . $this['bookable']['endInDays'] . " ";
720        if ($this['repeat']['afterWeeks']) {
721            $info .= " every " . $this['repeat']['afterWeeks'] . " week(s)";
722        }
723        if ($this['repeat']['weekOfMonth']) {
724            $info .= " each " . $this['repeat']['weekOfMonth'] . ". weekOfMonth";
725        }
726        $info .= " on ";
727        $weekdays = array_filter($this['weekday'], function ($value) {
728            return $value > 0;
729        });
730        $info .= implode(',', array_keys($weekdays));
731        $info .= " from " . $this->getStartDateTime()->format('H:i');
732        $info .= " to " . $this->getEndDateTime()->format('H:i');
733        $info .= " using " . $this['slotTimeInMinutes'] . "min slots";
734        $info .= " with p{$this['workstationCount']['public']}/";
735        $info .= "c{$this['workstationCount']['callcenter']}/";
736        $info .= "i{$this['workstationCount']['intern']}";
737        $day = $this->getSlotList()->getSummerizedSlot();
738        $info .= " day $day";
739        return $info;
740    }
741
742    /**
743     * Delete cache on changes
744     *
745     */
746    public function offsetSet($index, $value)
747    {
748        $this->startTimeCache = null;
749        $this->endTimeCache = null;
750        return parent::offsetSet($index, $value);
751    }
752
753    /**
754     * Check if availability is newer than given time
755     *
756     * @return bool
757     */
758    public function isNewerThan(\DateTimeInterface $dateTime)
759    {
760        return ($dateTime->getTimestamp() < $this->lastChange);
761    }
762
763    /**
764     * Reduce data of dereferenced entities to a required minimum
765     *
766     */
767    public function withLessData(array $keepArray = [])
768    {
769        $entity = clone $this;
770        if (! in_array('repeat', $keepArray)) {
771            unset($entity['repeat']);
772        }
773        if (! in_array('id', $keepArray)) {
774            unset($entity['id']);
775        }
776        if (! in_array('bookable', $keepArray)) {
777            unset($entity['bookable']);
778        }
779        if (! in_array('workstationCount', $keepArray)) {
780            unset($entity['workstationCount']);
781        }
782        if (! in_array('multipleSlotsAllowed', $keepArray)) {
783            unset($entity['multipleSlotsAllowed']);
784        }
785        if (! in_array('lastChange', $keepArray)) {
786            unset($entity['lastChange']);
787        }
788        if (! in_array('slotTimeInMinutes', $keepArray)) {
789            unset($entity['slotTimeInMinutes']);
790        }
791        if (! in_array('description', $keepArray)) {
792            unset($entity['description']);
793        }
794
795        return $entity;
796    }
797}