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