PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.1.3
Booking for Appointments and Events Calendar – Amelia v2.1.3
2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / src / Domain / Services / TimeSlot / TimeSlotService.php
ameliabooking / src / Domain / Services / TimeSlot Last commit date
TimeSlotService.php 5 months ago
TimeSlotService.php
1441 lines
1 <?php
2
3 namespace AmeliaBooking\Domain\Services\TimeSlot;
4
5 use AmeliaBooking\Domain\Common\Exceptions\InvalidArgumentException;
6 use AmeliaBooking\Domain\Entity\Booking\Appointment\Appointment;
7 use AmeliaBooking\Domain\Entity\Booking\Appointment\CustomerBooking;
8 use AmeliaBooking\Domain\Entity\Booking\SlotsEntities;
9 use AmeliaBooking\Domain\Entity\Schedule\DayOff;
10 use AmeliaBooking\Domain\Entity\Bookable\Service\Service;
11 use AmeliaBooking\Domain\Entity\User\Provider;
12 use AmeliaBooking\Domain\Services\DateTime\DateTimeService;
13 use AmeliaBooking\Domain\Collection\Collection;
14 use AmeliaBooking\Domain\Services\Entity\EntityService;
15 use AmeliaBooking\Domain\Services\Interval\IntervalService;
16 use AmeliaBooking\Domain\Services\Resource\AbstractResourceService;
17 use AmeliaBooking\Domain\Services\Schedule\ScheduleService;
18 use AmeliaBooking\Domain\Services\User\ProviderService;
19 use AmeliaBooking\Domain\ValueObjects\String\BookingStatus;
20 use AmeliaBooking\Domain\ValueObjects\String\Status;
21 use DateInterval;
22 use DatePeriod;
23 use DateTime;
24 use DateTimeZone;
25 use Exception;
26
27 /**
28 * Class TimeSlotService
29 *
30 * @package AmeliaBooking\Domain\Services\TimeSlot
31 */
32 class TimeSlotService
33 {
34 /** @var IntervalService */
35 private $intervalService;
36
37 /** @var ScheduleService */
38 private $scheduleService;
39
40 /** @var ProviderService */
41 private $providerService;
42
43 /** @var AbstractResourceService */
44 private $resourceService;
45
46 /** @var EntityService */
47 private $entityService;
48
49 /**
50 * TimeSlotService constructor.
51 *
52 * @param IntervalService $intervalService
53 * @param ScheduleService $scheduleService
54 * @param ProviderService $providerService
55 * @param AbstractResourceService $resourceService
56 * @param EntityService $entityService
57 */
58 public function __construct(
59 IntervalService $intervalService,
60 ScheduleService $scheduleService,
61 ProviderService $providerService,
62 AbstractResourceService $resourceService,
63 EntityService $entityService
64 ) {
65 $this->intervalService = $intervalService;
66
67 $this->scheduleService = $scheduleService;
68
69 $this->providerService = $providerService;
70
71 $this->resourceService = $resourceService;
72
73 $this->entityService = $entityService;
74 }
75
76 /** @noinspection MoreThanThreeArgumentsInspection */
77 /**
78 * get appointment intervals for provider.
79 *
80 * @param array $weekDaysIntervals
81 * @param array $intervals
82 * @param string $dateString
83 * @param int $start
84 * @param int $end
85 * @return array
86 */
87 private function getModifiedEndInterval($weekDaysIntervals, &$intervals, $dateString, $start, $end)
88 {
89 $dayIndex = DateTimeService::getDayIndex($dateString);
90
91 if (
92 isset($weekDaysIntervals[$dayIndex]['busy'][$start]) &&
93 $weekDaysIntervals[$dayIndex]['busy'][$start][1] > $end
94 ) {
95 $end = $weekDaysIntervals[$dayIndex]['busy'][$start][1];
96 }
97
98 if (
99 isset($intervals[$dateString]['occupied'][$start]) &&
100 $intervals[$dateString]['occupied'][$start][1] > $end
101 ) {
102 $end = $intervals[$dateString]['occupied'][$start][1];
103 }
104
105 return $end;
106 }
107
108 /**
109 * Split start and end in array of dates.
110 *
111 * @param DateTime $start
112 * @param DateTime $end
113 *
114 * @return array
115 */
116 private function getPeriodDates($start, $end)
117 {
118 /** @var DatePeriod $period */
119 $period = new DatePeriod(
120 $start->setTime(0, 0, 0),
121 new DateInterval('P1D'),
122 $end
123 );
124
125 $periodDates = [];
126
127 /** @var DateTime $date */
128 foreach ($period as $index => $date) {
129 $periodDates[] = $date->format('Y-m-d');
130 }
131
132 return $periodDates;
133 }
134
135 /** @noinspection MoreThanThreeArgumentsInspection */
136 /**
137 * get appointment intervals for provider.
138 *
139 * @param Provider $provider
140 * @param Collection $locations
141 * @param int $serviceId
142 * @param int $locationId
143 * @param int $personsCount
144 * @param boolean $bookIfPending
145 * @param boolean $bookOverApp
146 * @param array $weekDaysIntervals
147 * @param array $specialDaysIntervals
148 * @return array
149 * @throws InvalidArgumentException
150 */
151 private function getProviderAppointmentIntervals(
152 $provider,
153 $locations,
154 $serviceId,
155 $locationId,
156 $personsCount,
157 $bookIfPending,
158 $bookOverApp,
159 &$weekDaysIntervals,
160 &$specialDaysIntervals
161 ) {
162 $intervals = [];
163
164 $specialDays = [];
165
166 foreach ($specialDaysIntervals as $specialDay) {
167 $specialDays = array_merge($specialDays, $specialDay['dates']);
168 }
169
170 /** @var Appointment $app */
171 foreach ($provider->getAppointmentList()->getItems() as $app) {
172 $occupiedStart = $provider->getTimeZone() ?
173 DateTimeService::getDateTimeObjectInTimeZone(
174 $app->getBookingStart()->getValue()->format('Y-m-d H:i'),
175 $provider->getTimeZone()->getValue()
176 ) : DateTimeService::getCustomDateTimeObject($app->getBookingStart()->getValue()->format('Y-m-d H:i'));
177
178 $occupiedEnd = $provider->getTimeZone() ?
179 DateTimeService::getDateTimeObjectInTimeZone(
180 $app->getBookingEnd()->getValue()->format('Y-m-d H:i'),
181 $provider->getTimeZone()->getValue()
182 ) : DateTimeService::getCustomDateTimeObject($app->getBookingEnd()->getValue()->format('Y-m-d H:i'));
183
184 if ($app->getServiceId()->getValue()) {
185 $occupiedStart->modify('-' . ($app->getService()->getTimeBefore() ? $app->getService()->getTimeBefore()->getValue() : 0) . ' seconds');
186
187 $occupiedEnd->modify('+' . ($app->getService()->getTimeAfter() ? $app->getService()->getTimeAfter()->getValue() : 0) . ' seconds');
188 }
189
190 $occupiedDateStart = $occupiedStart->format('Y-m-d');
191
192 $occupiedSecondsStart = $this->intervalService->getSeconds($occupiedStart->format('H:i') . ':00');
193
194 $occupiedSecondsEnd = $this->intervalService->getSeconds($occupiedEnd->format('H:i:s'));
195
196 if (
197 $occupiedDateStart === $occupiedEnd->format('Y-m-d') &&
198 (!$bookOverApp || !$app->getServiceId()->getValue())
199 ) {
200 $intervals[$occupiedDateStart]['occupied'][$occupiedSecondsStart] = [
201 $occupiedSecondsStart,
202 $this->getModifiedEndInterval(
203 !array_key_exists($occupiedDateStart, $specialDays) ? $weekDaysIntervals : [],
204 $intervals,
205 $occupiedDateStart,
206 $occupiedSecondsStart,
207 $occupiedSecondsEnd
208 )
209 ];
210 } elseif (!$bookOverApp || !$app->getServiceId()->getValue()) {
211 $dates = $this->getPeriodDates($occupiedStart, $occupiedEnd);
212
213 $datesCount = sizeof($dates);
214
215 if ($datesCount === 1) {
216 $intervals[$dates[0]]['occupied'][$occupiedSecondsStart] = [
217 $occupiedSecondsStart,
218 $occupiedSecondsEnd === 0 ? 86400 : $occupiedSecondsEnd
219 ];
220 } else {
221 foreach ($dates as $index => $date) {
222 if ($index === 0) {
223 $intervals[$date]['occupied'][$occupiedSecondsStart] = [$occupiedSecondsStart, 86400];
224 } elseif ($index === $datesCount - 1) {
225 $modifiedEnd = $this->getModifiedEndInterval(
226 !array_key_exists($occupiedDateStart, $specialDays) ?
227 $weekDaysIntervals :
228 [],
229 $intervals,
230 $date,
231 0,
232 $occupiedSecondsEnd
233 );
234
235 $intervals[$date]['occupied'][0] = [
236 0,
237 $modifiedEnd === 0 ? 86400 : $modifiedEnd
238 ];
239 } else {
240 $intervals[$date]['occupied'][0] = [0, 86400];
241 }
242 }
243 }
244 }
245
246 $providerLocationId = $provider->getLocationId() ? $provider->getLocationId()->getValue() : null;
247
248 if ($app->getServiceId()->getValue() === $serviceId) {
249 $persons = 0;
250 $personsWaiting = 0;
251
252 /** @var CustomerBooking $booking */
253 foreach ($app->getBookings()->getItems() as $booking) {
254 if ($booking->getStatus()->getValue() !== BookingStatus::WAITING) {
255 $persons += $booking->getPersons()->getValue();
256 } else {
257 $personsWaiting += $booking->getPersons()->getValue();
258 }
259 }
260
261 $status = $app->getStatus()->getValue();
262
263 $appLocationId = $app->getLocationId() ? $app->getLocationId()->getValue() : null;
264
265 $hasCapacity =
266 $personsCount !== null &&
267 ($persons + $personsCount) <= $app->getService()->getMaxCapacity()->getValue() &&
268 !($app->isFull() ? $app->isFull()->getValue() : false);
269
270 $hasLocation =
271 !$locationId ||
272 ($app->getLocationId() && $app->getLocationId()->getValue() === $locationId) ||
273 (!$app->getLocationId() && $providerLocationId === $locationId) ||
274 ($appLocationId &&
275 $appLocationId === $locationId &&
276 $locations->getItem($appLocationId)->getStatus()->getValue() === Status::VISIBLE) ||
277 (!$appLocationId && $providerLocationId &&
278 $locations->getItem($providerLocationId)->getStatus()->getValue() === Status::VISIBLE);
279
280 $duration = $app->getBookingStart()->getValue()->diff($app->getBookingEnd()->getValue());
281
282 if (
283 ($hasLocation && $status === BookingStatus::APPROVED && $hasCapacity) ||
284 ($hasLocation && $status === BookingStatus::PENDING && ($bookIfPending || $hasCapacity))
285 ) {
286 $endDateTime = $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s');
287
288 $endDateTimeParts = explode(' ', $endDateTime);
289
290 $intervals[$occupiedDateStart]['available'][$app->getBookingStart()->getValue()->format('H:i')] =
291 [
292 'locationId' => $app->getLocationId() ?
293 $app->getLocationId()->getValue() : $providerLocationId,
294 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
295 'endDate' => $endDateTimeParts[0],
296 'endTime' => $endDateTimeParts[1],
297 'serviceId' => $serviceId,
298 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
299 ];
300 } else {
301 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
302 [
303 'locationId' => $app->getLocationId() ?
304 $app->getLocationId()->getValue() : $providerLocationId,
305 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
306 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
307 'serviceId' => $app->getServiceId()->getValue(),
308 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
309 'waiting' => $personsWaiting,
310 ];
311 }
312 } elseif ($app->getServiceId()->getValue()) {
313 $duration = $app->getBookingStart()->getValue()->diff($app->getBookingEnd()->getValue());
314
315 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
316 [
317 'locationId' => $app->getLocationId() ?
318 $app->getLocationId()->getValue() : $providerLocationId,
319 'places' => 0,
320 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
321 'serviceId' => $app->getServiceId()->getValue(),
322 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
323 ];
324 }
325 }
326
327 return $intervals;
328 }
329
330 /**
331 * get provider day off dates.
332 *
333 * @param Provider $provider
334 *
335 * @return array
336 * @throws Exception
337 */
338 private function getProviderDayOffDates($provider)
339 {
340 $dates = [];
341
342 /** @var DayOff $dayOff */
343 foreach ($provider->getDayOffList()->getItems() as $dayOff) {
344 $endDateCopy = clone $dayOff->getEndDate()->getValue();
345
346 $dayOffPeriod = new DatePeriod(
347 $dayOff->getStartDate()->getValue(),
348 new DateInterval('P1D'),
349 $endDateCopy->modify('+1 day')
350 );
351
352 /** @var DateTime $date */
353 foreach ($dayOffPeriod as $date) {
354 $dateFormatted = $dayOff->getRepeat()->getValue() ?
355 $date->format('m-d') :
356 $date->format('Y-m-d');
357
358 $dates[$dateFormatted] = $dateFormatted;
359 }
360 }
361
362 return $dates;
363 }
364
365 /**
366 * get available appointment intervals.
367 *
368 * @param array $availableIntervals
369 * @param array $unavailableIntervals
370 *
371 * @return array
372 */
373 private function getAvailableIntervals(&$availableIntervals, $unavailableIntervals)
374 {
375 $parsedAvailablePeriod = [];
376
377 ksort($availableIntervals);
378 ksort($unavailableIntervals);
379
380 foreach ($availableIntervals as $available) {
381 $parsedAvailablePeriod[] = $available;
382
383 foreach ($unavailableIntervals as $unavailable) {
384 if ($parsedAvailablePeriod) {
385 $lastAvailablePeriod = $parsedAvailablePeriod[sizeof($parsedAvailablePeriod) - 1];
386
387 if ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
388 // unavailable interval is inside available interval
389 $fixedPeriod = array_pop($parsedAvailablePeriod);
390
391 if ($fixedPeriod[0] !== $unavailable[0]) {
392 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
393 }
394
395 if ($unavailable[1] !== $fixedPeriod[1]) {
396 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
397 }
398 } elseif (
399 $unavailable[0] <= $lastAvailablePeriod[0] &&
400 $unavailable[1] >= $lastAvailablePeriod[1]
401 ) {
402 // available interval is inside unavailable interval
403 array_pop($parsedAvailablePeriod);
404 } elseif (
405 $unavailable[0] <= $lastAvailablePeriod[0] &&
406 $unavailable[1] >= $lastAvailablePeriod[0] &&
407 $unavailable[1] <= $lastAvailablePeriod[1]
408 ) {
409 // unavailable interval intersect start of available interval
410 $fixedPeriod = array_pop($parsedAvailablePeriod);
411
412 if ($unavailable[1] !== $fixedPeriod[1]) {
413 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
414 }
415 } elseif (
416 $unavailable[0] >= $lastAvailablePeriod[0] &&
417 $unavailable[0] <= $lastAvailablePeriod[1] &&
418 $unavailable[1] >= $lastAvailablePeriod[1]
419 ) {
420 // unavailable interval intersect end of available interval
421 $fixedPeriod = array_pop($parsedAvailablePeriod);
422
423 if ($fixedPeriod[0] !== $unavailable[0]) {
424 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
425 }
426 }
427 }
428 }
429 }
430
431 return $parsedAvailablePeriod;
432 }
433
434 /**
435 * @param Service $service
436 * @param Provider $provider
437 * @param int $personsCount
438 *
439 * @return bool
440 *
441 * @throws Exception
442 */
443 private function getOnlyAppointmentsSlots($service, $provider, $personsCount)
444 {
445 $getOnlyAppointmentsSlots = false;
446
447 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
448 /** @var Service $providerService */
449 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
450
451 if ($personsCount < $providerService->getMinCapacity()->getValue()) {
452 $getOnlyAppointmentsSlots = true;
453 }
454 }
455
456 return $getOnlyAppointmentsSlots;
457 }
458
459 /** @noinspection MoreThanThreeArgumentsInspection */
460 /**
461 * @param Service $service
462 * @param int $locationId
463 * @param Collection $providers
464 * @param Collection $locations
465 * @param array $globalDaysOffDates
466 * @param DateTime $startDateTime
467 * @param DateTime $endDateTime
468 * @param int $personsCount
469 * @param boolean $bookIfPending
470 * @param boolean $bookIfNotMin
471 * @param boolean $bookAfterMin
472 * @param boolean $bookOverApp
473 * @param array $appointmentsCount
474 * @param boolean $allowAdminBookAtAnytime
475 *
476 * @return array
477 * @throws Exception
478 */
479 private function getFreeTime(
480 Service $service,
481 $locationId,
482 Collection $locations,
483 Collection $providers,
484 array $globalDaysOffDates,
485 DateTime $startDateTime,
486 DateTime $endDateTime,
487 $personsCount,
488 $bookIfPending,
489 $bookIfNotMin,
490 $bookAfterMin,
491 $bookOverApp,
492 $appointmentsCount,
493 $allowAdminBookAtAnytime
494 ) {
495
496 $weekDayIntervals = [];
497
498 $appointmentIntervals = [];
499
500 $daysOffDates = [];
501
502 $specialDayIntervals = [];
503
504 $getOnlyAppointmentsSlots = [];
505
506 $serviceId = $service->getId()->getValue();
507
508 /** @var Provider $provider */
509 foreach ($providers->getItems() as $provider) {
510 $providerId = $provider->getId()->getValue();
511
512 $getOnlyAppointmentsSlots[$providerId] = $bookIfNotMin && $bookAfterMin ? $this->getOnlyAppointmentsSlots(
513 $service,
514 $provider,
515 $personsCount
516 ) : false;
517
518 $daysOffDates[$providerId] = $this->getProviderDayOffDates($provider);
519
520 $weekDayIntervals[$providerId] = $this->scheduleService->getProviderWeekDaysIntervals(
521 $provider,
522 $locations,
523 $locationId,
524 $serviceId
525 );
526
527 $specialDayIntervals[$providerId] = $this->scheduleService->getProviderSpecialDayIntervals(
528 $provider,
529 $locations,
530 $locationId,
531 $serviceId
532 );
533
534 $appointmentIntervals[$providerId] = $this->getProviderAppointmentIntervals(
535 $provider,
536 $locations,
537 $serviceId,
538 $locationId,
539 $personsCount,
540 $bookIfPending,
541 $bookOverApp,
542 $weekDayIntervals[$providerId],
543 $specialDayIntervals[$providerId]
544 );
545 }
546
547 $freeDateIntervals = [];
548
549 foreach ($appointmentIntervals as $providerKey => $providerDates) {
550 foreach ((array)$providerDates as $dateKey => $dateIntervals) {
551 $dayIndex = DateTimeService::getDayIndex($dateKey);
552
553 $specialDayDateKey = null;
554
555 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
556 if (array_key_exists($dateKey, $specialDays['dates']) && !empty($specialDays['intervals'])) {
557 $specialDayDateKey = $specialDayKey;
558 break;
559 }
560 }
561
562 if (
563 $specialDayDateKey !== null &&
564 isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])
565 ) {
566 // get free intervals if it is special day
567 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
568 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
569 !empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []
570 );
571 } elseif (
572 isset($weekDayIntervals[$providerKey][$dayIndex]['free']) &&
573 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
574 ) {
575 // get free intervals if it is working day
576 $unavailableIntervals =
577 $weekDayIntervals[$providerKey][$dayIndex]['busy'] + (!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []);
578
579 $intersectedTimes = array_intersect(
580 array_keys($weekDayIntervals[$providerKey][$dayIndex]['busy']),
581 array_keys(!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : [])
582 );
583
584 foreach ($intersectedTimes as $time) {
585 $unavailableIntervals[$time] =
586 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] >
587 $dateIntervals['occupied'][$time] ?
588 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] :
589 $dateIntervals['occupied'][$time];
590 }
591
592 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
593 $weekDayIntervals[$providerKey][$dayIndex]['free'],
594 $unavailableIntervals ?: []
595 );
596 }
597 }
598 }
599
600 $startDateTime = clone $startDateTime;
601
602 $startDateTime->setTime(0, 0);
603
604 $endDateTime = clone $endDateTime;
605
606 $endDateTime->modify('+1 day')->setTime(0, 0);
607
608 // create calendar
609 $period = new DatePeriod(
610 $startDateTime,
611 new DateInterval('P1D'),
612 $endDateTime
613 );
614
615 $calendar = [];
616
617 /** @var DateTime $day */
618 foreach ($period as $day) {
619 $currentDate = $day->format('Y-m-d');
620 $dayIndex = (int)$day->format('N');
621
622 $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) ||
623 array_key_exists($day->format('m-d'), $globalDaysOffDates);
624
625 if (!$isGlobalDayOff) {
626 foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) {
627 $isProviderDayOff = array_key_exists($currentDate, $daysOffDates[$providerKey]) ||
628 array_key_exists($day->format('m-d'), $daysOffDates[$providerKey]);
629
630 $specialDayDateKey = null;
631
632 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
633 if (
634 array_key_exists($currentDate, $specialDays['dates']) &&
635 !empty($specialDays['intervals'])
636 ) {
637 $specialDayDateKey = $specialDayKey;
638 break;
639 }
640 }
641
642 if (!$isProviderDayOff) {
643 // daily limit per employee
644 if (
645 !$allowAdminBookAtAnytime &&
646 !empty($appointmentsCount['limitCount']) &&
647 !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
648 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']
649 ) {
650 continue;
651 }
652
653 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
654 // get date intervals if there are appointments (special or working day)
655 $calendar[$currentDate][$providerKey] = [
656 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
657 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
658 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
659 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
660 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
661 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
662 ];
663 } else {
664 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
665 // get date intervals if it is special day with out appointments
666 $calendar[$currentDate][$providerKey] = [
667 'slots' => [],
668 'full' => [],
669 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
670 [] :
671 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
672 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
673 $appointmentsCount[$providerKey][$currentDate] :
674 0
675 ];
676 } elseif (
677 isset($weekDayIntervals[$providerKey][$dayIndex]) &&
678 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
679 ) {
680 // get date intervals if it is working day without appointments
681 $calendar[$currentDate][$providerKey] = [
682 'slots' => [],
683 'full' => [],
684 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
685 [] :
686 $weekDayIntervals[$providerKey][$dayIndex]['free'],
687 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
688 $appointmentsCount[$providerKey][$currentDate] :
689 0
690 ];
691 }
692 }
693 }
694 }
695 }
696 }
697
698 return $calendar;
699 }
700
701 /** @noinspection MoreThanThreeArgumentsInspection */
702 /**
703 * @param Service $service
704 * @param int $requiredTime
705 * @param array $freeIntervals
706 * @param array $resourcedIntervals
707 * @param int $slotLength
708 * @param DateTime $startDateTime
709 * @param bool $serviceDurationAsSlot
710 * @param bool $bufferTimeInSlot
711 * @param String $timeZone
712 * @param bool $structured
713 * @param array $customPricing
714 *
715 * @return array
716 * @throws Exception
717 */
718 private function getAppointmentFreeSlots(
719 $service,
720 $requiredTime,
721 &$freeIntervals,
722 $resourcedIntervals,
723 $slotLength,
724 $startDateTime,
725 $serviceDurationAsSlot,
726 $bufferTimeInSlot,
727 $timeZone,
728 $structured,
729 $customPricing
730 ) {
731 $availableResult = [];
732
733 $occupiedResult = [];
734
735 $realRequiredTime = $requiredTime -
736 $service->getTimeBefore()->getValue() -
737 $service->getTimeAfter()->getValue();
738
739 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
740 $requiredTime = $requiredTime -
741 $service->getTimeBefore()->getValue() -
742 $service->getTimeAfter()->getValue();
743 }
744
745 $currentDateTime = DateTimeService::getNowDateTimeObject();
746
747 $currentDateString = $currentDateTime->format('Y-m-d');
748
749 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
750
751 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
752
753 $currentDateFormatted = $currentDateTime->format('Y-m-d');
754
755 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
756
757 $startDateFormatted = $startDateTime->format('Y-m-d');
758
759 $bookingLength = $serviceDurationAsSlot ? $requiredTime : $slotLength;
760
761 $appCount = [];
762
763 $isContinuousTime = false;
764
765 $continuousTimeSlot = null;
766
767 foreach ($freeIntervals as $dateKey => $dateProviders) {
768 foreach ((array)$dateProviders as $providerKey => $provider) {
769 foreach ((array)$provider['intervals'] as $timePeriod) {
770 $freeIntervalEnd = $timePeriod[1];
771
772 $moveStart = false;
773
774 if ($timePeriod[0] === 0 && $isContinuousTime && $continuousTimeSlot !== null) {
775 $isContinuousTime = false;
776
777 $moveStart = true;
778 }
779
780 if ($timePeriod[1] === 86400) {
781 $nextDate = DateTimeService::getDateTimeObjectInTimeZone(
782 $dateKey . ' 00:00:00',
783 $timeZone
784 )->modify('+1 days');
785
786 $nextDateString = $nextDate->format('Y-m-d');
787
788 if (
789 $nextDate->format('j') !== '1' &&
790 isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
791 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
792 ) {
793 $isContinuousTime = true;
794
795 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
796
797 $timePeriod[1] += (
798 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
799 ? $realRequiredTime + $service->getTimeAfter()->getValue()
800 : $nextDayInterval);
801
802 $freeIntervalEnd = $timePeriod[1];
803 }
804 }
805
806 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
807 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
808 }
809
810 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
811
812 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
813
814 $numberOfSlots = (int)(
815 floor(
816 (
817 $timePeriod[1] -
818 $providerTimeStart -
819 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
820 ) / $bookingLength
821 ) + 1
822 );
823
824 $inspectResourceIndexes = [];
825
826 if (isset($resourcedIntervals[$dateKey])) {
827 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
828 if (
829 array_intersect(
830 $timePeriod[2],
831 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
832 )
833 ) {
834 $inspectResourceIndexes[] = $resourceIndex;
835 }
836 }
837 }
838
839 $providerPeriodSlots = [];
840
841 $achievedLength = 0;
842
843 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
844 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
845
846 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
847
848 $numberOfSlots = (int)(
849 floor(
850 (
851 $timePeriod[1] -
852 $providerTimeStart -
853 ($requiredTime - $service->getTimeBefore()->getValue())
854 ) / $bookingLength
855 ) + 1
856 );
857 }
858
859 if ($moveStart) {
860 $continuousTimeSlot = null;
861 }
862
863 for ($i = 0; $i < $numberOfSlots; $i++) {
864 $achievedLength += $bookingLength;
865
866 $timeSlot = $customerTimeStart + $i * $bookingLength;
867
868 if (
869 $timeSlot + $realRequiredTime + $service->getTimeAfter()->getValue() <= $freeIntervalEnd &&
870 (
871 $startDateFormatted !== $dateKey || (
872 $startTimeInSeconds < $timeSlot &&
873 (
874 $startDateFormatted !== $currentDateFormatted ||
875 $currentTimeInSeconds < $timeSlot
876 )
877 )
878 )
879 ) {
880 $timeSlotEnd = $timeSlot + $bookingLength;
881
882 $filteredLocationsIds = $timePeriod[2];
883
884 foreach ($inspectResourceIndexes as $resourceIndex) {
885 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
886 if (
887 ($start >= $timeSlot && $start < $timeSlotEnd) ||
888 ($end > $timeSlot && $end <= $timeSlotEnd) ||
889 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
890 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
891 ) {
892 $filteredLocationsIds = array_diff(
893 $filteredLocationsIds,
894 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
895 );
896
897 if (!$filteredLocationsIds) {
898 if ($achievedLength < $requiredTime) {
899 $providerPeriodSlots = [];
900
901 $achievedLength = 0;
902 }
903
904 continue 3;
905 }
906
907 $removedLocationsIds = array_diff(
908 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
909 $filteredLocationsIds
910 );
911
912 if ($removedLocationsIds && $achievedLength < $requiredTime) {
913 $parsedPeriodSlots = [];
914
915 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
916 if (
917 $start >= $previousTimeSlot &&
918 $start < $previousTimeSlot + $requiredTime
919 ) {
920 foreach ($periodSlotData as $data) {
921 if (!in_array($data[1], $removedLocationsIds)) {
922 $parsedPeriodSlots[$previousTimeSlot][] = $data;
923 }
924 }
925 } else {
926 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
927 }
928 }
929
930 $providerPeriodSlots = $parsedPeriodSlots;
931 }
932 }
933 }
934 }
935
936 if (!$timePeriod[2]) {
937 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
938 } elseif ($filteredLocationsIds) {
939 foreach ($filteredLocationsIds as $locationId) {
940 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
941 }
942 }
943 }
944 }
945
946 foreach ($providerPeriodSlots as $timeSlot => $data) {
947 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
948 . sprintf('%02d', floor(($timeSlot / 60) % 60));
949
950 if ($timeSlot <= 86400) {
951 if (!$structured) {
952 $availableResult[$dateKey][$time] = $data;
953 } else {
954 foreach ($data as $item) {
955 $availableResult[$dateKey][$time][] = [
956 'e' => $item[0],
957 'l' => $item[1],
958 'p' => $customPricing
959 ? $this->providerService->getDateTimePrice(
960 $customPricing,
961 $dateKey,
962 $timeSlot,
963 $timeZone
964 )
965 : null,
966 ];
967 }
968 }
969
970 if ($isContinuousTime) {
971 $continuousTimeSlot = $timeSlot;
972 }
973 }
974 }
975 }
976
977 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
978 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
979
980 if (
981 $currentDateString === $dateKey &&
982 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
983 ) {
984 continue;
985 }
986
987 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
988
989 $newEndInSeconds = $startInSeconds + $realRequiredTime;
990
991 if (
992 $newEndInSeconds !== 86400 &&
993 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
994 ) {
995 if ($dateKey !== $appointmentData['endDate']) {
996 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
997 $dateKey . ' 00:00:00',
998 $timeZone
999 )->modify('+1 days')->format('Y-m-d');
1000
1001 if (
1002 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1003 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
1004 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1005 ) {
1006 continue;
1007 }
1008 } elseif ($newEndInSeconds > 86400) {
1009 $nextIntervalIsValid = false;
1010
1011 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1012 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
1013 $nextIntervalIsValid = true;
1014
1015 break;
1016 }
1017 }
1018
1019 if (!$nextIntervalIsValid) {
1020 continue;
1021 }
1022
1023 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
1024 $dateKey . ' 00:00:00',
1025 $timeZone
1026 )->modify('+1 days')->format('Y-m-d');
1027
1028 if (
1029 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1030 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
1031 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1032 ) {
1033 continue;
1034 }
1035 } else {
1036 $nextIntervalIsValid = false;
1037
1038 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1039 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
1040 $nextIntervalIsValid = true;
1041
1042 break;
1043 }
1044 }
1045
1046 if (!$nextIntervalIsValid) {
1047 continue;
1048 }
1049 }
1050 }
1051
1052 $availableResult[$dateKey][$appointmentTime] = [
1053 !$structured ? [
1054 $providerKey,
1055 $appointmentData['locationId'],
1056 $appointmentData['places'],
1057 $appointmentData['serviceId'],
1058 $appointmentData['duration'],
1059 ] : [
1060 'e' => $providerKey,
1061 'l' => $appointmentData['locationId'],
1062 'c' => $appointmentData['places'],
1063 's' => $appointmentData['serviceId'],
1064 'd' => $appointmentData['duration'],
1065 'p' => $customPricing
1066 ? $this->providerService->getDateTimePrice(
1067 $customPricing,
1068 $dateKey,
1069 $this->intervalService->getSeconds($appointmentTime),
1070 $timeZone
1071 )
1072 : null,
1073 ]
1074 ];
1075 }
1076
1077 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
1078 $occupiedResult[$dateKey][$appointmentTime][] = !$structured ? [
1079 $providerKey,
1080 $appointmentData['locationId'],
1081 $appointmentData['places'],
1082 $appointmentData['serviceId'],
1083 $appointmentData['duration'],
1084 ] : [
1085 'e' => $providerKey,
1086 'l' => $appointmentData['locationId'],
1087 'c' => $appointmentData['places'],
1088 's' => $appointmentData['serviceId'],
1089 'd' => $appointmentData['duration'],
1090 'w' => $appointmentData['waiting'] ?? 0,
1091 ];
1092 }
1093
1094 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
1095 }
1096 }
1097
1098 return [
1099 'available' => $availableResult,
1100 'occupied' => $occupiedResult,
1101 'appCount' => $appCount
1102 ];
1103 }
1104
1105 /**
1106 * @param array $slots
1107 * @param string $timeZone
1108 *
1109 * @return array
1110 * @throws Exception
1111 */
1112 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
1113 {
1114 $convertedProviderSlots = [];
1115
1116 foreach ($slots as $slotDate => $slotTimes) {
1117 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
1118 $convertedSlotParts = explode(
1119 ' ',
1120 DateTimeService::getDateTimeObjectInTimeZone(
1121 $slotDate . ' ' . $slotTime,
1122 $timeZone
1123 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
1124 );
1125
1126 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1127 }
1128 }
1129
1130 return $convertedProviderSlots;
1131 }
1132
1133
1134 /**
1135 * @param Collection $appointments
1136 * @param int $excludeAppointmentId
1137 *
1138 * @return array
1139 * @throws Exception
1140 */
1141 public function getAppointmentCount($appointments, $excludeAppointmentId)
1142 {
1143 $appCount = [];
1144
1145 /** @var Appointment $appointment */
1146 foreach ($appointments->getItems() as $appointment) {
1147 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1148 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1149 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1150 } else {
1151 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1152 }
1153 }
1154 }
1155
1156 return $appCount;
1157 }
1158
1159 /** @noinspection MoreThanThreeArgumentsInspection */
1160 /**
1161 * @param array $settings
1162 * @param array $props
1163 * @param SlotsEntities $slotsEntities
1164 * @param Collection $appointments
1165 *
1166 * @return array
1167 * @throws Exception
1168 */
1169 public function getSlots($settings, $props, $slotsEntities, $appointments)
1170 {
1171 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1172
1173 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1174 $this->resourceService->manageResources(
1175 $slotsEntities->getResources(),
1176 $appointments,
1177 $slotsEntities->getLocations(),
1178 $slotsEntities->getServices()->getItem($props['serviceId']),
1179 $slotsEntities->getProviders(),
1180 $props['locationId'],
1181 $props['excludeAppointmentId'],
1182 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1183 ) : [];
1184
1185 $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1186
1187 $this->providerService->addAppointmentsToAppointmentList(
1188 $slotsEntities->getProviders(),
1189 $appointments,
1190 $settings['isGloballyBusySlot']
1191 );
1192
1193 return $this->getCalculatedFreeSlots(
1194 $settings,
1195 $props,
1196 $slotsEntities,
1197 $resourcedLocationsIntervals,
1198 $appointmentsCount
1199 );
1200 }
1201
1202 /** @noinspection MoreThanThreeArgumentsInspection */
1203 /**
1204 * @param array $settings
1205 * @param array $props
1206 * @param SlotsEntities $slotsEntities
1207 * @param array $resourcedLocationsIntervals
1208 * @param array $appointmentsCount
1209 *
1210 * @return array
1211 * @throws Exception
1212 */
1213 private function getCalculatedFreeSlots(
1214 $settings,
1215 $props,
1216 $slotsEntities,
1217 $resourcedLocationsIntervals,
1218 $appointmentsCount
1219 ) {
1220 $freeProvidersSlots = [];
1221
1222 /** @var DateTime $startDateTime */
1223 $startDateTime = $props['startDateTime'];
1224
1225 /** @var DateTime $endDateTime */
1226 $endDateTime = $props['endDateTime'];
1227
1228 /** @var Service $service */
1229 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1230
1231 /** @var Collection $providers */
1232 $providers = $slotsEntities->getProviders();
1233
1234 /** @var Collection $locations */
1235 $locations = $slotsEntities->getLocations();
1236
1237 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1238 $service,
1239 $props['extras']
1240 );
1241
1242 /** @var Provider $provider */
1243 foreach ($providers->getItems() as $provider) {
1244 /** @var Service $providerService */
1245 $providerService = $service;
1246
1247 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1248 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1249
1250 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1251 continue;
1252 }
1253 }
1254
1255 $customPricing = $this->providerService->getCustomPricing(
1256 $providerService,
1257 $provider->getTimeZone()
1258 ? $provider->getTimeZone()->getValue()
1259 : DateTimeService::getTimeZone()->getName()
1260 );
1261
1262 $providerContainer = new Collection();
1263
1264 if ($provider->getTimeZone()) {
1265 $this->providerService->modifyProviderTimeZone(
1266 $provider,
1267 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1268 $startDateTime,
1269 $endDateTime
1270 );
1271 }
1272
1273 $start = $provider->getTimeZone() ?
1274 DateTimeService::getCustomDateTimeObjectInTimeZone(
1275 $startDateTime->format('Y-m-d H:i'),
1276 $provider->getTimeZone()->getValue()
1277 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1278
1279 $end = $provider->getTimeZone() ?
1280 DateTimeService::getCustomDateTimeObjectInTimeZone(
1281 $endDateTime->format('Y-m-d H:i'),
1282 $provider->getTimeZone()->getValue()
1283 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1284
1285 $providerContainer->addItem($provider, $provider->getId()->getValue());
1286
1287 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1288 $settings['limitPerEmployee']['numberOfApp'] : null;
1289
1290 $freeIntervals = $this->getFreeTime(
1291 $service,
1292 $props['locationId'],
1293 $locations,
1294 $providerContainer,
1295 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1296 [] : $settings['globalDaysOff'],
1297 $start,
1298 $end,
1299 $props['personsCount'],
1300 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1301 $settings['allowBookingIfNotMin'],
1302 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1303 !empty($settings['allowAdminBookOverApp']),
1304 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount],
1305 !empty($settings['allowAdminBookAtAnyTime'])
1306 );
1307
1308 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1309 $service,
1310 $requiredTime,
1311 $freeIntervals,
1312 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1313 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1314 $settings['timeSlotLength'] ?: $requiredTime,
1315 $start,
1316 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1317 $settings['serviceDurationAsSlot'],
1318 $settings['bufferTimeInSlot'],
1319 $provider->getTimeZone() ?
1320 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName(),
1321 !empty($props['structured']),
1322 $customPricing
1323 );
1324 }
1325
1326 $freeSlots = [
1327 'available' => [],
1328 'occupied' => [],
1329 'appCount' => [],
1330 'duration' => $requiredTime / 60,
1331 ];
1332
1333 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1334 /** @var Provider $provider */
1335 $provider = $providers->getItem($providerKey);
1336
1337 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1338
1339 if (!empty($settings['allowAdminBookOverApp']) && !$props['isFrontEndBooking'] && $props['structured']) {
1340 $this->setBookedTimeSlots($providerSlots);
1341 }
1342
1343 foreach (['available', 'occupied'] as $type) {
1344 if ($provider->getTimeZone()) {
1345 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1346 $providerSlots[$type],
1347 $provider->getTimeZone()->getValue()
1348 );
1349 }
1350
1351 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1352 foreach ($dateSlots as $timeKey => $slotData) {
1353 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1354 $freeSlots[$type][$dateKey][$timeKey] = [];
1355 }
1356
1357 foreach ($slotData as $item) {
1358 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1359 }
1360
1361 if (isset($freeSlots[$type][$dateKey])) {
1362 if (!$freeSlots[$type][$dateKey]) {
1363 unset($freeSlots[$type][$dateKey]);
1364 } else {
1365 ksort($freeSlots[$type][$dateKey]);
1366 }
1367 }
1368 }
1369 }
1370 }
1371 }
1372
1373 return $freeSlots;
1374 }
1375
1376 /**
1377 * @param array $freeSlots
1378 *
1379 * @throws Exception
1380 */
1381 private function setBookedTimeSlots(&$freeSlots)
1382 {
1383 foreach (['available', 'occupied'] as $type) {
1384 foreach ($freeSlots[$type] as $dateString => $timeSlots) {
1385 foreach ($timeSlots as $timeString => $slots) {
1386 foreach ($slots as $slot) {
1387 if (isset($slot['d'])) {
1388 $appointmentStart = $this->intervalService->getSeconds($timeString . ':00') / 60;
1389
1390 $isSameDay = $appointmentStart + $slot['d'] <= 1440;
1391
1392 $appointmentEnd = $isSameDay
1393 ? $appointmentStart + $slot['d']
1394 : $appointmentStart + $slot['d'] - 1440;
1395
1396 if (isset($freeSlots['available'][$dateString])) {
1397 foreach ($freeSlots['available'][$dateString] as $inspectedTimeString => $inspectedSlotData) {
1398 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1399
1400 if (
1401 $inspectedSlot > $appointmentStart &&
1402 $inspectedSlot < ($isSameDay ? $appointmentEnd : 1440)
1403 ) {
1404 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1405 if ($slot['e'] === $inspectedSlot['e']) {
1406 $freeSlots['available'][$dateString][$inspectedTimeString][$inspectedIndex]['i'] = true;
1407
1408 break;
1409 }
1410 }
1411 }
1412 }
1413 }
1414
1415 if (!$isSameDay) {
1416 $nextDateString = (
1417 new \DateTime($dateString, DateTimeService::getTimeZone())
1418 )->modify('+1 day')->format('Y-m-d');
1419
1420 if (isset($freeSlots['available'][$nextDateString])) {
1421 foreach ($freeSlots['available'][$nextDateString] as $inspectedTimeString => $inspectedSlotData) {
1422 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1423
1424 if ($inspectedSlot < $appointmentEnd) {
1425 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1426 if ($slot['e'] === $inspectedSlot['e']) {
1427 $freeSlots['available'][$nextDateString][$inspectedTimeString][$inspectedIndex] = true;
1428 }
1429 }
1430 }
1431 }
1432 }
1433 }
1434 }
1435 }
1436 }
1437 }
1438 }
1439 }
1440 }
1441