Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.23% |
316 / 470 |
|
78.95% |
30 / 38 |
CRAP | |
0.00% |
0 / 1 |
Availability | |
67.23% |
316 / 470 |
|
78.95% |
30 / 38 |
1173.71 | |
0.00% |
0 / 1 |
getDefaults | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
1 | |||
hasDate | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
hasBookableDates | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
isOpenedOnDate | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
8 | |||
isOpened | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
hasWeekDay | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
hasAppointment | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
hasTime | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getAvailableSecondsPerDay | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
hasDay | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
hasDayOff | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
hasWeek | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
10 | |||
getStartDateTime | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getEndDateTime | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getDuration | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getBookableStart | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
getBookableEnd | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
isBookable | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
7 | |||
hasDateBetween | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
validateStartTime | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
306 | |||
validateWeekdays | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
110 | |||
validateEndTime | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
validateOriginEndTime | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
72 | |||
validateType | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
validateSlotTime | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
validateBookableDayRange | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getSlotList | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
getSlotTimeInMinutes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConflict | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
isMatchOf | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
15 | |||
hasSharedWeekdayWith | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
9 | |||
getTimeOverlaps | |
100.00% |
38 / 38 |
|
100.00% |
1 / 1 |
12 | |||
withCalculatedSlots | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
withScope | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
__toString | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
3 | |||
offsetSet | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
isNewerThan | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
withLessData | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
9 |
1 | <?php |
2 | |
3 | namespace BO\Zmsentities; |
4 | |
5 | /** |
6 | * @SuppressWarnings(Complexity) |
7 | * @SuppressWarnings(Coupling) |
8 | * @SuppressWarnings(PublicMethod) |
9 | * |
10 | */ |
11 | class 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 | } |