Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.23% covered (warning)
67.23%
316 / 470
78.95% covered (warning)
78.95%
30 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
Availability
67.23% covered (warning)
67.23%
316 / 470
78.95% covered (warning)
78.95%
30 / 38
1173.71
0.00% covered (danger)
0.00%
0 / 1
 getDefaults
100.00% covered (success)
100.00%
34 / 34
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
 hasDateBetween
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 validateStartTime
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
306
 validateWeekdays
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
110
 validateEndTime
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 validateOriginEndTime
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
72
 validateType
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 validateSlotTime
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 validateBookableDayRange
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 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
 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    public 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            'scope' => [
72                'id' => 123,
73                'provider' => [
74                    'id' => 0,
75                    'name' => '',
76                    'source' => ''
77                ],
78                'shortName' => ''
79            ]
80        ];
81    }
82
83    /**
84     * Check, if the dateTime contains a day given by the settings
85     * ATTENTION: Time critical function, keep highly optimized
86     * Compared to isOpened() the Booking time is checked too
87     *
88     * @param \DateTimeInterface $dateTime
89     *
90     * @return Bool
91     */
92    public function hasDate(\DateTimeInterface $dateTime, \DateTimeInterface $now)
93    {
94        $dateTime = Helper\DateTime::create($dateTime);
95        if (
96            !$this->isOpenedOnDate($dateTime)
97            || !$this->isBookable($dateTime, $now)
98        ) {
99            // Out of date range
100            return false;
101        }
102        return true;
103    }
104
105    public function hasBookableDates(\DateTimeInterface $now)
106    {
107        if ($this->workstationCount['intern'] <= 0) {
108            return false;
109        }
110        if ($this->getEndDateTime()->getTimestamp() < $now->getTimestamp()) {
111            return false;
112        }
113        $stopDate = $this->getBookableEnd($now);
114        if ($this->getStartDateTime()->getTimestamp() > $stopDate->getTimestamp()) {
115            return false;
116        }
117        return $this->hasDateBetween($this->getBookableStart($now), $this->getBookableEnd($now), $now);
118    }
119
120    /**
121     * Check, if the dateTime contains a day
122     * ATTENTION: Time critical function, keep highly optimized
123     *
124     * @param \DateTimeInterface $dateTime
125     * @param String $type of "openinghours", "appointment" or false to ignore type
126     *
127     * @return Bool
128     */
129    public function isOpenedOnDate(\DateTimeInterface $dateTime, $type = false)
130    {
131        $dateTime = Helper\DateTime::create($dateTime);
132        if (
133            !$this->hasWeekDay($dateTime)
134            || ($type !== false && $this->type != $type)
135            || !$this->hasDay($dateTime)
136            || !$this->hasWeek($dateTime)
137            || ($this->getDuration() > 2 && $this->hasDayOff($dateTime))
138        ) {
139            // Out of date range
140            return false;
141        }
142        return true;
143    }
144
145    /**
146     * Check if date and time is in availability
147     * Compared to hasDate() the time of the day is checked, but not booking time
148     *
149     * @param \DateTimeInterface $dateTime
150     * @param String $type of "openinghours", "appointment" or false to ignore type
151     *
152     */
153    public function isOpened(\DateTimeInterface $dateTime, $type = false)
154    {
155        return (!$this->isOpenedOnDate($dateTime, $type) || !$this->hasTime($dateTime)) ? false : true;
156    }
157
158    public function hasWeekDay(\DateTimeInterface $dateTime)
159    {
160        $weekDayName = self::$weekdayNameList[$dateTime->format('w')];
161        if (!$this['weekday'][$weekDayName]) {
162            // Wrong weekday
163            return false;
164        }
165        return true;
166    }
167
168    public function hasAppointment(Appointment $appointment)
169    {
170        $dateTime = $appointment->toDateTime();
171        $isOpenedStart = $this->isOpened($dateTime, false);
172        $duration = $this->slotTimeInMinutes * $appointment->slotCount;
173        $endTime = $dateTime->modify("+" . $duration . "minutes")
174            ->modify("-1 second"); // To allow the last slot for an appointment
175        $isOpenedEnd = $this->isOpened($endTime, false);
176        return ($isOpenedStart && $isOpenedEnd);
177    }
178
179    /**
180     * Check, if the dateTime is a time covered by availability
181     *
182     * @param \DateTimeInterface $dateTime
183     *
184     * @return Bool
185     */
186    public function hasTime(\DateTimeInterface $dateTime)
187    {
188        $start = $this->getStartDateTime()->getSecondsOfDay();
189        $end = $this->getEndDateTime()->getSecondsOfDay();
190        $compare = Helper\DateTime::create($dateTime)->getSecondsOfDay();
191        if ($start > $compare || $end <= $compare) {
192            // Out of time range
193            return false;
194        }
195        return true;
196    }
197
198    public function getAvailableSecondsPerDay($type = "intern")
199    {
200        $start = $this->getStartDateTime()->getSecondsOfDay();
201        $end = $this->getEndDateTime()->getSecondsOfDay();
202        return ($end - $start) * $this->workstationCount[$type];
203    }
204
205    /**
206     * Check, if the dateTime is a day covered by availability
207     *
208     * @param \DateTimeInterface $dateTime
209     *
210     * @return Bool
211     */
212    public function hasDay(\DateTimeInterface $dateTime)
213    {
214        $start = $this->getStartDateTime()->modify('0:00:00');
215        $end = $this->getEndDateTime()->modify('23:59:59');
216        if ($dateTime->getTimestamp() < $start->getTimestamp() || $dateTime->getTimestamp() > $end->getTimestamp()) {
217            // Out of date range
218            return false;
219        }
220        return true;
221    }
222
223    /**
224     * Check, if the dateTime is a dayoff date
225     *
226     * @param \DateTimeInterface $dateTime
227     *
228     * @return Bool
229     */
230    public function hasDayOff(\DateTimeInterface $dateTime)
231    {
232        if (isset($this['scope']['dayoff'])) {
233            $timeStamp = $dateTime->format('Y-m-d');
234            foreach ($this['scope']['dayoff'] as $dayOff) {
235                if (date('Y-m-d', $dayOff['date']) == $timeStamp) {
236                    return true;
237                }
238            }
239        } else {
240            throw new Exception\DayoffMissing();
241        }
242        return false;
243    }
244
245    /**
246     * Check, if the dateTime contains a week given by the week repetition settings
247     *
248     * @param \DateTimeInterface $dateTime
249     *
250     * @return Bool
251     */
252    public function hasWeek(\DateTimeInterface $dateTime)
253    {
254        $dateTime = Helper\DateTime::create($dateTime);
255        $start = $this->getStartDateTime();
256        $monday = "monday this week";
257        if (
258            $this['repeat']['afterWeeks']
259            && ($this['repeat']['afterWeeks'] == 1
260                || 0 ===
261                    $dateTime->modify($monday)->diff($start->modify($monday))->days
262                 % ($this['repeat']['afterWeeks'] * 7)
263            )
264        ) {
265            return true;
266        }
267        if (
268            $this['repeat']['weekOfMonth']
269            && (
270                $dateTime->isWeekOfMonth($this['repeat']['weekOfMonth'])
271                // On a value of 5, always take the last week
272                || ($this['repeat']['weekOfMonth'] >= 5 && $dateTime->isLastWeekOfMonth())
273            )
274        ) {
275            return true;
276        }
277        if (!$this['repeat']['weekOfMonth'] && !$this['repeat']['afterWeeks']) {
278            return true;
279        }
280        return false;
281    }
282
283    /**
284     * Get DateTimeInterface for start time of availability
285     *
286     * @return \DateTimeInterface
287     */
288    public function getStartDateTime()
289    {
290        if (!$this->startTimeCache) {
291            $this->startTimeCache = Helper\DateTime::create()
292                ->setTimestamp($this['startDate'])
293                ->modify('today ' .  $this['startTime']);
294        }
295        return $this->startTimeCache;
296    }
297
298    /**
299     * Get DateTimeInterface for end time of availability
300     *
301     * @return \DateTimeInterface
302     */
303    public function getEndDateTime()
304    {
305        if (!$this->endTimeCache) {
306            $this->endTimeCache = Helper\DateTime::create()
307                ->setTimestamp($this['endDate'])
308                ->modify('today ' .  $this['endTime']);
309        }
310        return $this->endTimeCache;
311    }
312
313    /**
314     * Get duration of availability
315     *
316     * @return integer
317     */
318    public function getDuration()
319    {
320        $startTime = $this->getStartDateTime();
321        $endTime = $this->getEndDateTime();
322        return (int)$endTime->diff($startTime)->format("%a");
323    }
324
325    /**
326     * Get DateTimeInterface for start booking time of availability
327     *
328     * @param \DateTimeInterface $now relative time to compare booking settings
329     *
330     * @return \DateTimeInterface
331     */
332    public function getBookableStart(\DateTimeInterface $now)
333    {
334        $now = Helper\DateTime::create($now);
335        $availabilityStart = Helper\Property::create($this)->bookable->startInDays->get();
336        $time = $this->getStartDateTime()->format('H:i:s');
337        if (null !== $availabilityStart) {
338            return $now->modify('+' . $availabilityStart . 'days')->modify($time);
339        }
340        $scopeStart = Helper\Property::create($this)->scope->preferences->appointment->startInDaysDefault->get();
341        if (null !== $scopeStart) {
342            return $now->modify('+' . $scopeStart . 'days')->modify($time);
343        }
344        throw new \BO\Zmsentities\Exception\ProcessBookableFailed(
345            "Undefined start time for booking, try to set the scope properly"
346        );
347    }
348
349    /**
350     * Get DateTimeInterface for end booking time of availability
351     *
352     * @param \DateTimeInterface $now relative time to compare booking settings
353     *
354     * @return \DateTimeInterface
355     */
356    public function getBookableEnd(\DateTimeInterface $now)
357    {
358        $now = Helper\DateTime::create($now);
359        $availabilityEnd = Helper\Property::create($this)->bookable->endInDays->get();
360        $time = $this->getEndDateTime()->format('H:i:s');
361        if (null !== $availabilityEnd) {
362            return $now->modify('+' . $availabilityEnd . 'days')->modify($time);
363        }
364        $scopeEnd = Helper\Property::create($this)->scope->preferences->appointment->endInDaysDefault->get();
365        if (null !== $scopeEnd) {
366            return $now->modify('+' . $scopeEnd . 'days')->modify($time);
367        }
368        throw new \BO\Zmsentities\Exception\ProcessBookableFailed(
369            "Undefined end time for booking, try to set the scope properly"
370        );
371    }
372
373    /**
374     * Check, if the dateTime contains is within the bookable range (usually for public access)
375     * The current time is used to compare the start Time of the availability
376     *
377     * @param \DateTimeInterface $dateTime
378     * @param \DateTimeInterface $now relative time to compare booking settings
379     *
380     * @return Bool
381     */
382    public function isBookable(\DateTimeInterface $bookableDate, \DateTimeInterface $now)
383    {
384        if (!$this->hasDay($bookableDate)) {
385            return false;
386        }
387        $bookableCurrentTime = $bookableDate->modify($now->format('H:i:s'));
388        Helper\DateTime::create($bookableDate)->getTimestamp() + Helper\DateTime::create($now)->getSecondsOfDay();
389        $startDate = $this->getBookableStart($now)->modify('00:00:00');
390
391        if ($bookableCurrentTime->getTimestamp() < $startDate->getTimestamp()) {
392            //error_log("START " . $bookableCurrentTime->format('c').'<'.$startDate->format('c'). " " . $this);
393            return false;
394        }
395        $endDate = $this->getBookableEnd($now)->modify('23:59:59');
396        if ($bookableCurrentTime->getTimestamp() > $endDate->getTimestamp()) {
397            //error_log("END " . $bookableCurrentTime->format('c').'>'.$endDate->format('c'). " " . $this);
398            return false;
399        }
400        if (
401            $bookableDate->format('Y-m-d') == $endDate->format('Y-m-d')
402            && $now->format('Y-m-d') != $this->getEndDateTime()->format('Y-m-d')
403        ) {
404            // Avoid releasing all appointments on midnight, allow smaller contingents distributed over the day
405            $delayedStart = $this->getBookableEnd($now)->modify($this->getStartDateTime()->format('H:i:s'));
406            if ($bookableCurrentTime->getTimestamp() < $delayedStart->getTimestamp()) {
407                //error_log(
408                //    sprintf("DELAY %s<%s", $bookableCurrentTime->format('c'), $delayedStart->format('c'))
409                //    ." $this"
410                //);
411                return false;
412            }
413        }
414        return true;
415    }
416
417    /**
418     * Check, if a day between two dates is included
419     *
420     * @return Array of arrays with the keys time, public, callcenter, intern
421     */
422    public function hasDateBetween(\DateTimeInterface $startTime, \DateTimeInterface $stopTime, \DateTimeInterface $now): bool
423    {
424        if ($startTime->getTimestamp() < $now->getTimestamp()) {
425            $startTime = $now;
426        }
427        if ($stopTime->getTimestamp() < $now->getTimestamp()) {
428            return false;
429        }
430        do {
431            if ($this->hasDate($startTime, $now)) {
432                return true;
433            }
434            $startTime = $startTime->modify('+1 day');
435        } while ($startTime->getTimestamp() <= $stopTime->getTimestamp());
436        return false;
437    }
438
439    public function validateStartTime(\DateTimeInterface $today, \DateTimeInterface $tomorrow, \DateTimeInterface $startDate, \DateTimeInterface $endDate, \DateTimeInterface $selectedDate, string $kind): array
440    {
441        $errorList = [];
442
443        $startTime = (clone $startDate)->setTime(0, 0);
444        $startHour = (int) $startDate->format('H');
445        $endHour = (int) $endDate->format('H');
446        $startMinute = (int) $startDate->format('i');
447        $endMinute = (int) $endDate->format('i');
448        $isFuture = ($kind && $kind === 'future');
449
450        if (
451            !$isFuture &&
452            $selectedDate->getTimestamp() > $today->getTimestamp() &&
453            $startTime->getTimestamp() > (clone $selectedDate)->setTime(0, 0)->getTimestamp()
454        ) {
455            $errorList[] = [
456                'type' => 'startTimeFuture',
457                'message' => "Das Startdatum der Ã–ffnungszeit muss vor dem " . $tomorrow->format('d.m.Y') . " liegen."
458            ];
459        }
460
461        if (
462            ($startHour === 22 && $startMinute > 0) ||
463            $startHour === 23 ||
464            $startHour === 0 ||
465            ($endHour === 22 && $endMinute > 0) ||
466            $endHour === 23 ||
467            $endHour === 0 ||
468            ($startHour === 1 && $startMinute > 0) ||
469            ($endHour === 1 && $endMinute > 0)
470        ) {
471            $errorList[] = [
472                'type' => 'startOfDay',
473                'message' => 'Die Uhrzeit darf nicht zwischen 22:00 und 01:00 liegen, da in diesem Zeitraum der tägliche Cronjob ausgeführt wird.'
474            ];
475        }
476
477        return $errorList;
478    }
479
480    public function validateWeekdays(\DateTimeInterface $startDate, \DateTimeInterface $endDate, array $weekday, string $kind): array
481    {
482        $errorList = [];
483
484        // Skip validation if this is part of a split series
485        if ($kind === 'origin' || $kind === 'future') {
486            return $errorList;
487        }
488
489        if ($startDate > $endDate) {
490            return $errorList;
491        }
492
493        $hasSelectedDay = false;
494        foreach (self::$weekdayNameList as $day) {
495            if ((int)$weekday[$day] > 0) {
496                $hasSelectedDay = true;
497                break;
498            }
499        }
500
501        if (!$hasSelectedDay) {
502            $errorList[] = [
503                'type' => 'weekdayRequired',
504                'message' => 'Mindestens ein Wochentag muss ausgewählt sein.'
505            ];
506            return $errorList;
507        }
508
509        $germanWeekdays = [
510            'sunday' => 'Sonntag',
511            'monday' => 'Montag',
512            'tuesday' => 'Dienstag',
513            'wednesday' => 'Mittwoch',
514            'thursday' => 'Donnerstag',
515            'friday' => 'Freitag',
516            'saturday' => 'Samstag'
517        ];
518
519        $selectedWeekdays = array_filter(self::$weekdayNameList, function ($day) use ($weekday) {
520            return (int)$weekday[$day] > 0;
521        });
522        $foundWeekdays = [];
523
524        $currentDate = clone $startDate;
525        while ($currentDate <= $endDate) {
526            $weekDayName = self::$weekdayNameList[$currentDate->format('w')];
527            if (in_array($weekDayName, $selectedWeekdays)) {
528                $foundWeekdays[] = $weekDayName;
529            }
530            $currentDate = $currentDate->modify('+1 day');
531        }
532
533        $missingWeekdays = array_diff($selectedWeekdays, array_unique($foundWeekdays));
534        if (!empty($missingWeekdays)) {
535            $germanMissingWeekdays = array_map(function ($day) use ($germanWeekdays) {
536                return $germanWeekdays[$day];
537            }, $missingWeekdays);
538
539            $errorList[] = [
540                'type' => 'invalidWeekday',
541                'message' => sprintf(
542                    'Die ausgewählten Wochentage (%s) kommen im gewählten Zeitraum nicht vor.',
543                    implode(', ', $germanMissingWeekdays)
544                )
545            ];
546        }
547
548        return $errorList;
549    }
550
551    public function validateEndTime(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
552    {
553        $errorList = [];
554
555        $startHour = (int) $startDate->format('H');
556        $endHour = (int) $endDate->format('H');
557        $startMinute = (int) $startDate->format('i');
558        $endMinute = (int) $endDate->format('i');
559        $dayMinutesStart = ($startHour * 60) + $startMinute;
560        $dayMinutesEnd = ($endHour * 60) + $endMinute;
561        $startTimestamp = $startDate->getTimestamp();
562        $endTimestamp = $endDate->getTimestamp();
563
564        if ($dayMinutesEnd <= $dayMinutesStart) {
565            $errorList[] = [
566                'type' => 'endTime',
567                'message' => 'Die Endzeit darf nicht vor der Startzeit liegen.'
568            ];
569        } elseif ($startTimestamp >= $endTimestamp) {
570            $errorList[] = [
571                'type' => 'endTime',
572                'message' => 'Das Enddatum darf nicht vor dem Startdatum liegen.'
573            ];
574        }
575
576        return $errorList;
577    }
578
579    public function validateOriginEndTime(\DateTimeInterface $today, \DateTimeInterface $yesterday, \DateTimeInterface $endDate, \DateTimeInterface $selectedDate, string $kind): array
580    {
581        $errorList = [];
582        $endHour = (int) $endDate->format('H');
583        $endMinute = (int) $endDate->format('i');
584        $startDate = $this->getStartDateTime();
585        $startHour = (int) $startDate->format('H');
586        $startMinute = (int) $startDate->format('i');
587        $endDateTime = (clone $endDate)->setTime($endHour, $endMinute);
588        $startDateTime = (clone $startDate)->setTime($startHour, $startMinute);
589        $endTimestamp = $endDateTime->getTimestamp();
590        $startTimestamp = $startDateTime->getTimestamp();
591        $isOrigin = ($kind && $kind === 'origin');
592
593        if (!$isOrigin && $selectedDate->getTimestamp() > $today->getTimestamp() && $endDate < (clone $selectedDate)->setTime(0, 0)) {
594            $errorList[] = [
595                'type' => 'endTimeFuture',
596                'message' => "Das Enddatum der Ã–ffnungszeit muss nach dem " . $yesterday->format('d.m.Y') . " liegen."
597            ];
598        }
599
600        if (!$isOrigin && $startTimestamp < $today->getTimestamp() && $endTimestamp < $today->getTimestamp()) {
601            $errorList[] = [
602                'type' => 'endTimePast',
603                'message' => 'Öffnungszeiten in der Vergangenheit lassen sich nicht bearbeiten '
604                    . '(Die aktuelle Zeit "' . $today->format('d.m.Y H:i') . ' Uhr" liegt nach dem Terminende am "'
605                    . $endDateTime->format('d.m.Y H:i') . ' Uhr" und dem Terminanfang am "'
606                    . $startDateTime->format('d.m.Y H:i') . ' Uhr").'
607            ];
608        }
609
610        return $errorList;
611    }
612
613    public function validateType(string $kind): array
614    {
615        $errorList = [];
616        if (empty($kind)) {
617            $errorList[] = [
618                'type' => 'type',
619                'message' => 'Typ erforderlich'
620            ];
621        }
622        return $errorList;
623    }
624
625    public function validateSlotTime(\DateTimeInterface $startDate, \DateTimeInterface $endDate): array
626    {
627        $errorList = [];
628        $slotTime = $this['slotTimeInMinutes'];
629
630        $startHour = (int)$startDate->format('H');
631        $startMinute = (int)$startDate->format('i');
632        $endHour = (int)$endDate->format('H');
633        $endMinute = (int)$endDate->format('i');
634
635        $totalMinutes = (($endHour - $startHour) * 60) + ($endMinute - $startMinute);
636
637        if ($slotTime === 0) {
638            $errorList[] = [
639                'type' => 'slotTime',
640                'message' => 'Die Slot-Zeit darf nicht 0 sein.'
641            ];
642            return $errorList;
643        }
644
645        if ($totalMinutes % $slotTime > 0) {
646            $errorList[] = [
647                'type' => 'slotCount',
648                'message' => 'Zeitschlitze müssen sich gleichmäßig in der Ã–ffnungszeit aufteilen lassen.'
649            ];
650        }
651
652        return $errorList;
653    }
654
655    public function validateBookableDayRange(int $startInDays, int $endInDays): array
656    {
657        $errorList = [];
658        if ($startInDays > $endInDays) {
659            $errorList[] = [
660                'type' => 'bookableDayRange',
661                'message' => 'Bitte geben Sie im Feld \'von\' eine kleinere Zahl ein als im Feld \'bis\', wenn Sie bei \'Buchbar\' sind.'
662            ];
663        }
664
665        return $errorList;
666    }
667
668    /**
669     * Creates a list of slots available on a valid day
670     *
671     * @return Array of arrays with the keys time, public, callcenter, intern
672     */
673    public function getSlotList()
674    {
675        $startTime = Helper\DateTime::create($this['startTime']);
676        $stopTime = Helper\DateTime::create($this['endTime']);
677        $slotList = new Collection\SlotList();
678        $slotInstance = new Slot($this['workstationCount']);
679        if ($this['slotTimeInMinutes'] > 0) {
680            do {
681                $slot = clone $slotInstance;
682                $slot->setTime($startTime);
683                $slotList[] = $slot;
684                $startTime = $startTime->modify('+' . $this['slotTimeInMinutes'] . 'minute');
685                // Only add a slot, if at least a minute is left, otherwise do not ("<" instead "<=")
686            } while ($startTime->getTimestamp() < $stopTime->getTimestamp());
687        }
688        return $slotList;
689    }
690
691    public function getSlotTimeInMinutes()
692    {
693        return $this['slotTimeInMinutes'];
694    }
695
696    /**
697     * Get problems on configuration of this availability
698     *
699     * @return Collection\ProcessList with processes in status "conflict"
700     */
701    public function getConflict()
702    {
703        $start = $this->getStartDateTime()->getSecondsOfDay();
704        $end = $this->getEndDateTime()->getSecondsOfDay();
705        $minutesPerDay = floor(($end - $start) / 60);
706        if ($minutesPerDay % $this->slotTimeInMinutes > 0) {
707            $conflict = new Process();
708            $conflict->status = 'conflict';
709            $appointment = $conflict->getFirstAppointment();
710            $appointment->availability = $this;
711            $appointment->date = $this->getStartDateTime()->getTimestamp();
712            $conflict->amendment =
713                "Der eingestellte Zeitschlitz von {$this->slotTimeInMinutes} Minuten"
714                . " sollte in die eingestellte Uhrzeit passen.";
715            return $conflict;
716        }
717        return false;
718    }
719
720    /**
721     * Check of a different availability has the same opening configuration
722     *
723     */
724    public function isMatchOf(Availability $availability)
725    {
726        return ($this->type != $availability->type
727            || $this->startTime != $availability->startTime
728            || $this->endTime != $availability->endTime
729            || $this->startDate != $availability->startDate
730            || $this->endDate != $availability->endDate
731            || $this->repeat['afterWeeks'] != $availability->repeat['afterWeeks']
732            || $this->repeat['weekOfMonth'] != $availability->repeat['weekOfMonth']
733            || (bool)$this->weekday['monday'] != (bool)$availability->weekday['monday']
734            || (bool)$this->weekday['tuesday'] != (bool)$availability->weekday['tuesday']
735            || (bool)$this->weekday['wednesday'] != (bool)$availability->weekday['wednesday']
736            || (bool)$this->weekday['thursday'] != (bool)$availability->weekday['thursday']
737            || (bool)$this->weekday['friday'] != (bool)$availability->weekday['friday']
738            || (bool)$this->weekday['saturday'] != (bool)$availability->weekday['saturday']
739            || (bool)$this->weekday['sunday'] != (bool)$availability->weekday['sunday']
740        ) ? false : true;
741    }
742
743    public function hasSharedWeekdayWith(Availability $availability)
744    {
745        return ($this->type == $availability->type
746            && (bool)$this->weekday['monday'] != (bool)$availability->weekday['monday']
747            && (bool)$this->weekday['tuesday'] != (bool)$availability->weekday['tuesday']
748            && (bool)$this->weekday['wednesday'] != (bool)$availability->weekday['wednesday']
749            && (bool)$this->weekday['thursday'] != (bool)$availability->weekday['thursday']
750            && (bool)$this->weekday['friday'] != (bool)$availability->weekday['friday']
751            && (bool)$this->weekday['saturday'] != (bool)$availability->weekday['saturday']
752            && (bool)$this->weekday['sunday'] != (bool)$availability->weekday['sunday']
753        ) ? false : true;
754    }
755
756    /**
757     * Get overlaps on daytime
758     * This functions does not check, if two availabilities are openend on the same day!
759     *
760     * @param Availability $availability for comparision
761     *
762     * @return Collection\ProcessList with processes in status "conflict"
763     *
764     *
765     */
766
767    /*
768    1
769    Case 01:  |-----|
770              |-----|
771                 2
772
773                 1
774    Case 02:  |-----|
775                 |-----|
776                    2
777
778                    1
779    Case 03:     |-----|
780              |-----|
781                 2
782
783                   1
784    Case 04:  |---------|
785                |-----|
786                   2
787
788                   1
789    Case 05:    |-----|
790              |---------|
791                   2
792
793                 1
794    Case 06:  |-----|
795                      |-----|
796                         2
797
798                         1
799    Case 07:          |-----|
800              |-----|
801                 2
802
803                 1
804    Case 08:  |-----|
805                    |-----|
806                       2
807
808                       1
809    Case 09:        |-----|
810              |-----|
811                 2
812
813                 1
814    Case 10:     |
815              |-----|
816                 2
817
818                 1
819    Case 11:  |-----|
820                 |
821                 2
822
823              1
824    Case 12:  |
825              |-----|
826                 2
827
828                    1
829    Case 13:        |
830              |-----|
831                 2
832
833                 1
834    Case 14:  |-----|
835              |
836              2
837
838                 1
839    Case 15:  |-----|
840                    |
841                    2
842
843              1
844    Case 16:  |
845              |
846              2
847
848            |                         |    Operlap    |     Overlap
849      Case  |         Example         | Open Interval | Closed Interval
850    --------|-------------------------|---------------|-----------------
851    Case 01 | 09:00-11:00 09:00-11:00 |      Yes      |        Yes
852    Case 02 | 09:00-11:00 10:00-12:00 |      Yes      |        Yes
853    Case 03 | 10:00-12:00 09:00-11:00 |      Yes      |        Yes
854    Case 04 | 09:00-12:00 10:00-11:00 |      Yes      |        Yes
855    Case 05 | 10:00-11:00 09:00-12:00 |      Yes      |        Yes
856    Case 06 | 09:00-10:00 11:00-12:00 |      No       |        No
857    Case 07 | 11:00-12:00 09:00-10:00 |      No       |        No
858    Case 08 | 09:00-10:00 10:00-11:00 |      No       |        Yes
859    Case 09 | 10:00-11:00 09:00-10:00 |      No       |        Yes
860    Case 10 | 10:00-10:00 09:00-11:00 |      Yes      |        Yes
861    Case 11 | 09:00-11:00 10:00-10:00 |      Yes      |        Yes
862    Case 12 | 09:00-09:00 09:00-10:00 |      No       |        Yes
863    Case 13 | 10:00-10:00 09:00-10:00 |      No       |        Yes
864    Case 14 | 09:00-10:00 09:00-09:00 |      No       |        Yes
865    Case 15 | 09:00-10:00 10:00-10:00 |      No       |        Yes
866    Case 16 | 09:00-09:00 09:00-09:00 |      No       |        Yes
867    */
868
869    public function getTimeOverlaps(Availability $availability, \DateTimeInterface $currentDate)
870    {
871        $processList = new Collection\ProcessList();
872        if (
873            $availability->id != $this->id
874            && $availability->type == $this->type
875            && $this->hasSharedWeekdayWith($availability)
876        ) {
877            $processTemplate = new Process();
878            $processTemplate->amendment = "Zwei Ã–ffnungszeiten Ã¼berschneiden sich.";
879            $processTemplate->status = 'conflict';
880            $appointment = $processTemplate->getFirstAppointment();
881            $appointment->availability = $this;
882            $appointment->date = $this->getStartDateTime()->getTimestamp();
883            $thisStart = $this->getStartDateTime()->getSecondsOfDay();
884            $thisEnd = $this->getEndDateTime()->getSecondsOfDay();
885            $availabilityStart = $availability->getStartDateTime()->getSecondsOfDay();
886            $availabilityEnd = $availability->getEndDateTime()->getSecondsOfDay();
887
888            $isEqual = ($availabilityStart == $thisStart && $availabilityEnd == $thisEnd);
889
890            if ($availabilityStart < $thisEnd && $thisStart < $availabilityEnd && ! $isEqual) {
891                $process = clone $processTemplate;
892                $process->getFirstAppointment()->date = $this
893                    ->getStartDateTime()
894                    ->modify($currentDate->format("Y-m-d"))
895                    ->getTimestamp();
896                $processList->addEntity($process);
897            } elseif ($thisEnd < $availabilityStart && $availabilityEnd < $thisStart && ! $isEqual) {
898                $process = clone $processTemplate;
899                $process->getFirstAppointment()->date = $availability
900                    ->getStartDateTime()
901                    ->modify($currentDate->format("Y-m-d"))
902                    ->getTimestamp();
903                $processList->addEntity($process);
904            } elseif ($isEqual) {
905                $process = clone $processTemplate;
906                $process->amendment = "Zwei Ã–ffnungszeiten sind gleich.";
907                $process->getFirstAppointment()->date = $availability
908                    ->getStartDateTime()
909                    ->modify($currentDate->format("Y-m-d"))
910                    ->getTimestamp();
911                $processList->addEntity($process);
912            }
913        }
914        return $processList;
915    }
916
917    /**
918     * Update workstationCount to number of calculated appointments
919     *
920     * @return self cloned
921     */
922    public function withCalculatedSlots()
923    {
924        $availability = clone $this;
925        $startTime = Helper\DateTime::create($this['startTime']);
926        $stopTime = Helper\DateTime::create($this['endTime']);
927        $openingSeconds = $stopTime->getTimestamp() - $startTime->getTimestamp();
928        $openingMinutes = floor($openingSeconds / 60);
929        $slices = 0;
930        if ($this['slotTimeInMinutes'] > 0) {
931            $slices = floor($openingMinutes / $this['slotTimeInMinutes']);
932        }
933        $slot = new Slot([
934            'type' => Slot::FREE,
935            'intern' => $this['workstationCount']['intern'] * $slices,
936            'callcenter' => $this['workstationCount']['callcenter'] * $slices,
937            'public' => $this['workstationCount']['public'] * $slices,
938        ]);
939        $availability['workstationCount'] = $slot;
940        return $availability;
941    }
942
943    public function withScope(\BO\Zmsentities\Scope $scope)
944    {
945        $availability = clone $this;
946        $availability->scope = $scope;
947        return $availability;
948    }
949
950    public function __toString()
951    {
952        $info = "Availability." . $this['type'] . " #" . $this['id'];
953        $info .= " starting " . $this->startDate . $this->getStartDateTime()->format(' Y-m-d');
954        $info .= "||now+" . $this['bookable']['startInDays'] . " ";
955        $info .= " until " . $this->getEndDateTime()->format('Y-m-d');
956        $info .= "||now+" . $this['bookable']['endInDays'] . " ";
957        if ($this['repeat']['afterWeeks']) {
958            $info .= " every " . $this['repeat']['afterWeeks'] . " week(s)";
959        }
960        if ($this['repeat']['weekOfMonth']) {
961            $info .= " each " . $this['repeat']['weekOfMonth'] . ". weekOfMonth";
962        }
963        $info .= " on ";
964        $weekdays = array_filter($this['weekday'], function ($value) {
965            return $value > 0;
966        });
967        $info .= implode(',', array_keys($weekdays));
968        $info .= " from " . $this->getStartDateTime()->format('H:i');
969        $info .= " to " . $this->getEndDateTime()->format('H:i');
970        $info .= " using " . $this['slotTimeInMinutes'] . "min slots";
971        $info .= " with p{$this['workstationCount']['public']}/";
972        $info .= "c{$this['workstationCount']['callcenter']}/";
973        $info .= "i{$this['workstationCount']['intern']}";
974        $day = $this->getSlotList()->getSummerizedSlot();
975        $info .= " day $day";
976        return $info;
977    }
978
979    /**
980     * Delete cache on changes
981     *
982     */
983    public function offsetSet($index, $value)
984    {
985        $this->startTimeCache = null;
986        $this->endTimeCache = null;
987        return parent::offsetSet($index, $value);
988    }
989
990    /**
991     * Check if availability is newer than given time
992     *
993     * @return bool
994     */
995    public function isNewerThan(\DateTimeInterface $dateTime)
996    {
997        return ($dateTime->getTimestamp() < $this->lastChange);
998    }
999
1000    /**
1001     * Reduce data of dereferenced entities to a required minimum
1002     *
1003     */
1004    public function withLessData(array $keepArray = [])
1005    {
1006        $entity = clone $this;
1007        if (! in_array('repeat', $keepArray)) {
1008            unset($entity['repeat']);
1009        }
1010        if (! in_array('id', $keepArray)) {
1011            unset($entity['id']);
1012        }
1013        if (! in_array('bookable', $keepArray)) {
1014            unset($entity['bookable']);
1015        }
1016        if (! in_array('workstationCount', $keepArray)) {
1017            unset($entity['workstationCount']);
1018        }
1019        if (! in_array('multipleSlotsAllowed', $keepArray)) {
1020            unset($entity['multipleSlotsAllowed']);
1021        }
1022        if (! in_array('lastChange', $keepArray)) {
1023            unset($entity['lastChange']);
1024        }
1025        if (! in_array('slotTimeInMinutes', $keepArray)) {
1026            unset($entity['slotTimeInMinutes']);
1027        }
1028        if (! in_array('description', $keepArray)) {
1029            unset($entity['description']);
1030        }
1031
1032        return $entity;
1033    }
1034}