PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.0.1
Booking for Appointments and Events Calendar – Amelia v2.0.1
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 8 months ago
TimeSlotService.php
1427 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'])) {
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 (array_key_exists($currentDate, $specialDays['dates'])) {
634 $specialDayDateKey = $specialDayKey;
635 break;
636 }
637 }
638
639 if (!$isProviderDayOff) {
640 // daily limit per employee
641 if (
642 !$allowAdminBookAtAnytime &&
643 !empty($appointmentsCount['limitCount']) &&
644 !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
645 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']
646 ) {
647 continue;
648 }
649
650 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
651 // get date intervals if there are appointments (special or working day)
652 $calendar[$currentDate][$providerKey] = [
653 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
654 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
655 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
656 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
657 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
658 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
659 ];
660 } else {
661 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
662 // get date intervals if it is special day with out appointments
663 $calendar[$currentDate][$providerKey] = [
664 'slots' => [],
665 'full' => [],
666 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
667 [] :
668 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
669 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
670 $appointmentsCount[$providerKey][$currentDate] :
671 0
672 ];
673 } elseif (
674 isset($weekDayIntervals[$providerKey][$dayIndex]) &&
675 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
676 ) {
677 // get date intervals if it is working day without appointments
678 $calendar[$currentDate][$providerKey] = [
679 'slots' => [],
680 'full' => [],
681 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
682 [] :
683 $weekDayIntervals[$providerKey][$dayIndex]['free'],
684 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
685 $appointmentsCount[$providerKey][$currentDate] :
686 0
687 ];
688 }
689 }
690 }
691 }
692 }
693 }
694
695 return $calendar;
696 }
697
698 /** @noinspection MoreThanThreeArgumentsInspection */
699 /**
700 * @param Service $service
701 * @param int $requiredTime
702 * @param array $freeIntervals
703 * @param array $resourcedIntervals
704 * @param int $slotLength
705 * @param DateTime $startDateTime
706 * @param bool $serviceDurationAsSlot
707 * @param bool $bufferTimeInSlot
708 * @param String $timeZone
709 * @param bool $structured
710 * @param array $customPricing
711 *
712 * @return array
713 * @throws Exception
714 */
715 private function getAppointmentFreeSlots(
716 $service,
717 $requiredTime,
718 &$freeIntervals,
719 $resourcedIntervals,
720 $slotLength,
721 $startDateTime,
722 $serviceDurationAsSlot,
723 $bufferTimeInSlot,
724 $timeZone,
725 $structured,
726 $customPricing
727 ) {
728 $availableResult = [];
729
730 $occupiedResult = [];
731
732 $realRequiredTime = $requiredTime -
733 $service->getTimeBefore()->getValue() -
734 $service->getTimeAfter()->getValue();
735
736 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
737 $requiredTime = $requiredTime -
738 $service->getTimeBefore()->getValue() -
739 $service->getTimeAfter()->getValue();
740 }
741
742 $currentDateTime = DateTimeService::getNowDateTimeObject();
743
744 $currentDateString = $currentDateTime->format('Y-m-d');
745
746 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
747
748 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
749
750 $currentDateFormatted = $currentDateTime->format('Y-m-d');
751
752 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
753
754 $startDateFormatted = $startDateTime->format('Y-m-d');
755
756 $bookingLength = $serviceDurationAsSlot ? $requiredTime : $slotLength;
757
758 $appCount = [];
759
760 $isContinuousTime = false;
761
762 $continuousTimeSlot = null;
763
764 foreach ($freeIntervals as $dateKey => $dateProviders) {
765 foreach ((array)$dateProviders as $providerKey => $provider) {
766 foreach ((array)$provider['intervals'] as $timePeriod) {
767 $moveStart = false;
768
769 if ($timePeriod[0] === 0 && $isContinuousTime && $continuousTimeSlot !== null) {
770 $isContinuousTime = false;
771
772 $moveStart = true;
773 }
774
775 if ($timePeriod[1] === 86400) {
776 $nextDate = DateTimeService::getDateTimeObjectInTimeZone(
777 $dateKey . ' 00:00:00',
778 $timeZone
779 )->modify('+1 days');
780
781 $nextDateString = $nextDate->format('Y-m-d');
782
783 if (
784 $nextDate->format('j') !== '1' &&
785 isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
786 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
787 ) {
788 $isContinuousTime = true;
789
790 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
791
792 $timePeriod[1] += (
793 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
794 ? $realRequiredTime + $service->getTimeAfter()->getValue()
795 : $nextDayInterval);
796 }
797 }
798
799 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
800 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
801 }
802
803 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
804
805 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
806
807 $numberOfSlots = (int)(
808 floor(
809 (
810 $timePeriod[1] -
811 $providerTimeStart -
812 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
813 ) / $bookingLength
814 ) + 1
815 );
816
817 $inspectResourceIndexes = [];
818
819 if (isset($resourcedIntervals[$dateKey])) {
820 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
821 if (
822 array_intersect(
823 $timePeriod[2],
824 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
825 )
826 ) {
827 $inspectResourceIndexes[] = $resourceIndex;
828 }
829 }
830 }
831
832 $providerPeriodSlots = [];
833
834 $achievedLength = 0;
835
836 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
837 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
838
839 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
840
841 $numberOfSlots = (int)(floor(($timePeriod[1] - $providerTimeStart - $requiredTime) / $bookingLength) + 1);
842 }
843
844 if ($moveStart) {
845 $continuousTimeSlot = null;
846 }
847
848 for ($i = 0; $i < $numberOfSlots; $i++) {
849 $achievedLength += $bookingLength;
850
851 $timeSlot = $customerTimeStart + $i * $bookingLength;
852
853 if (
854 (
855 $startDateFormatted !== $dateKey &&
856 ($serviceDurationAsSlot &&
857 !$bufferTimeInSlot ? $timeSlot <= $timePeriod[1] - $requiredTime : true)
858 ) ||
859 ($startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot) ||
860 ($startDateFormatted ===
861 $currentDateFormatted &&
862 $startDateFormatted === $dateKey &&
863 $startTimeInSeconds < $timeSlot &&
864 $currentTimeInSeconds < $timeSlot)
865 ) {
866 $timeSlotEnd = $timeSlot + $bookingLength;
867
868 $filteredLocationsIds = $timePeriod[2];
869
870 foreach ($inspectResourceIndexes as $resourceIndex) {
871 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
872 if (
873 ($start >= $timeSlot && $start < $timeSlotEnd) ||
874 ($end > $timeSlot && $end <= $timeSlotEnd) ||
875 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
876 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
877 ) {
878 $filteredLocationsIds = array_diff(
879 $filteredLocationsIds,
880 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
881 );
882
883 if (!$filteredLocationsIds) {
884 if ($achievedLength < $requiredTime) {
885 $providerPeriodSlots = [];
886
887 $achievedLength = 0;
888 }
889
890 continue 3;
891 }
892
893 $removedLocationsIds = array_diff(
894 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
895 $filteredLocationsIds
896 );
897
898 if ($removedLocationsIds && $achievedLength < $requiredTime) {
899 $parsedPeriodSlots = [];
900
901 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
902 if (
903 $start >= $previousTimeSlot &&
904 $start < $previousTimeSlot + $requiredTime
905 ) {
906 foreach ($periodSlotData as $data) {
907 if (!in_array($data[1], $removedLocationsIds)) {
908 $parsedPeriodSlots[$previousTimeSlot][] = $data;
909 }
910 }
911 } else {
912 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
913 }
914 }
915
916 $providerPeriodSlots = $parsedPeriodSlots;
917 }
918 }
919 }
920 }
921
922 if (!$timePeriod[2]) {
923 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
924 } elseif ($filteredLocationsIds) {
925 foreach ($filteredLocationsIds as $locationId) {
926 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
927 }
928 }
929 }
930 }
931
932 foreach ($providerPeriodSlots as $timeSlot => $data) {
933 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
934 . sprintf('%02d', floor(($timeSlot / 60) % 60));
935
936 if ($timeSlot <= 86400) {
937 if (!$structured) {
938 $availableResult[$dateKey][$time] = $data;
939 } else {
940 foreach ($data as $item) {
941 $availableResult[$dateKey][$time][] = [
942 'e' => $item[0],
943 'l' => $item[1],
944 'p' => $customPricing
945 ? $this->providerService->getDateTimePrice(
946 $customPricing,
947 $dateKey,
948 $timeSlot,
949 $timeZone
950 )
951 : null,
952 ];
953 }
954 }
955
956 if ($isContinuousTime) {
957 $continuousTimeSlot = $timeSlot;
958 }
959 }
960 }
961 }
962
963 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
964 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
965
966 if (
967 $currentDateString === $dateKey &&
968 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
969 ) {
970 continue;
971 }
972
973 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
974
975 $newEndInSeconds = $startInSeconds + $realRequiredTime;
976
977 if (
978 $newEndInSeconds !== 86400 &&
979 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
980 ) {
981 if ($dateKey !== $appointmentData['endDate']) {
982 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
983 $dateKey . ' 00:00:00',
984 $timeZone
985 )->modify('+1 days')->format('Y-m-d');
986
987 if (
988 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
989 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
990 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
991 ) {
992 continue;
993 }
994 } elseif ($newEndInSeconds > 86400) {
995 $nextIntervalIsValid = false;
996
997 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
998 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
999 $nextIntervalIsValid = true;
1000
1001 break;
1002 }
1003 }
1004
1005 if (!$nextIntervalIsValid) {
1006 continue;
1007 }
1008
1009 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
1010 $dateKey . ' 00:00:00',
1011 $timeZone
1012 )->modify('+1 days')->format('Y-m-d');
1013
1014 if (
1015 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1016 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
1017 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1018 ) {
1019 continue;
1020 }
1021 } else {
1022 $nextIntervalIsValid = false;
1023
1024 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1025 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
1026 $nextIntervalIsValid = true;
1027
1028 break;
1029 }
1030 }
1031
1032 if (!$nextIntervalIsValid) {
1033 continue;
1034 }
1035 }
1036 }
1037
1038 $availableResult[$dateKey][$appointmentTime] = [
1039 !$structured ? [
1040 $providerKey,
1041 $appointmentData['locationId'],
1042 $appointmentData['places'],
1043 $appointmentData['serviceId'],
1044 $appointmentData['duration'],
1045 ] : [
1046 'e' => $providerKey,
1047 'l' => $appointmentData['locationId'],
1048 'c' => $appointmentData['places'],
1049 's' => $appointmentData['serviceId'],
1050 'd' => $appointmentData['duration'],
1051 'p' => $customPricing
1052 ? $this->providerService->getDateTimePrice(
1053 $customPricing,
1054 $dateKey,
1055 $this->intervalService->getSeconds($appointmentTime),
1056 $timeZone
1057 )
1058 : null,
1059 ]
1060 ];
1061 }
1062
1063 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
1064 $occupiedResult[$dateKey][$appointmentTime][] = !$structured ? [
1065 $providerKey,
1066 $appointmentData['locationId'],
1067 $appointmentData['places'],
1068 $appointmentData['serviceId'],
1069 $appointmentData['duration'],
1070 ] : [
1071 'e' => $providerKey,
1072 'l' => $appointmentData['locationId'],
1073 'c' => $appointmentData['places'],
1074 's' => $appointmentData['serviceId'],
1075 'd' => $appointmentData['duration'],
1076 'w' => $appointmentData['waiting'] ?? 0,
1077 ];
1078 }
1079
1080 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
1081 }
1082 }
1083
1084 return [
1085 'available' => $availableResult,
1086 'occupied' => $occupiedResult,
1087 'appCount' => $appCount
1088 ];
1089 }
1090
1091 /**
1092 * @param array $slots
1093 * @param string $timeZone
1094 *
1095 * @return array
1096 * @throws Exception
1097 */
1098 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
1099 {
1100 $convertedProviderSlots = [];
1101
1102 foreach ($slots as $slotDate => $slotTimes) {
1103 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
1104 $convertedSlotParts = explode(
1105 ' ',
1106 DateTimeService::getDateTimeObjectInTimeZone(
1107 $slotDate . ' ' . $slotTime,
1108 $timeZone
1109 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
1110 );
1111
1112 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1113 }
1114 }
1115
1116 return $convertedProviderSlots;
1117 }
1118
1119
1120 /**
1121 * @param Collection $appointments
1122 * @param int $excludeAppointmentId
1123 *
1124 * @return array
1125 * @throws Exception
1126 */
1127 public function getAppointmentCount($appointments, $excludeAppointmentId)
1128 {
1129 $appCount = [];
1130
1131 /** @var Appointment $appointment */
1132 foreach ($appointments->getItems() as $appointment) {
1133 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1134 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1135 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1136 } else {
1137 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1138 }
1139 }
1140 }
1141
1142 return $appCount;
1143 }
1144
1145 /** @noinspection MoreThanThreeArgumentsInspection */
1146 /**
1147 * @param array $settings
1148 * @param array $props
1149 * @param SlotsEntities $slotsEntities
1150 * @param Collection $appointments
1151 *
1152 * @return array
1153 * @throws Exception
1154 */
1155 public function getSlots($settings, $props, $slotsEntities, $appointments)
1156 {
1157 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1158
1159 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1160 $this->resourceService->manageResources(
1161 $slotsEntities->getResources(),
1162 $appointments,
1163 $slotsEntities->getLocations(),
1164 $slotsEntities->getServices()->getItem($props['serviceId']),
1165 $slotsEntities->getProviders(),
1166 $props['locationId'],
1167 $props['excludeAppointmentId'],
1168 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1169 ) : [];
1170
1171 $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1172
1173 $this->providerService->addAppointmentsToAppointmentList(
1174 $slotsEntities->getProviders(),
1175 $appointments,
1176 $settings['isGloballyBusySlot']
1177 );
1178
1179 return $this->getCalculatedFreeSlots(
1180 $settings,
1181 $props,
1182 $slotsEntities,
1183 $resourcedLocationsIntervals,
1184 $appointmentsCount
1185 );
1186 }
1187
1188 /** @noinspection MoreThanThreeArgumentsInspection */
1189 /**
1190 * @param array $settings
1191 * @param array $props
1192 * @param SlotsEntities $slotsEntities
1193 * @param array $resourcedLocationsIntervals
1194 * @param array $appointmentsCount
1195 *
1196 * @return array
1197 * @throws Exception
1198 */
1199 private function getCalculatedFreeSlots(
1200 $settings,
1201 $props,
1202 $slotsEntities,
1203 $resourcedLocationsIntervals,
1204 $appointmentsCount
1205 ) {
1206 $freeProvidersSlots = [];
1207
1208 /** @var DateTime $startDateTime */
1209 $startDateTime = $props['startDateTime'];
1210
1211 /** @var DateTime $endDateTime */
1212 $endDateTime = $props['endDateTime'];
1213
1214 /** @var Service $service */
1215 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1216
1217 /** @var Collection $providers */
1218 $providers = $slotsEntities->getProviders();
1219
1220 /** @var Collection $locations */
1221 $locations = $slotsEntities->getLocations();
1222
1223 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1224 $service,
1225 $props['extras']
1226 );
1227
1228 /** @var Provider $provider */
1229 foreach ($providers->getItems() as $provider) {
1230 /** @var Service $providerService */
1231 $providerService = $service;
1232
1233 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1234 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1235
1236 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1237 continue;
1238 }
1239 }
1240
1241 $customPricing = $this->providerService->getCustomPricing(
1242 $providerService,
1243 $provider->getTimeZone()
1244 ? $provider->getTimeZone()->getValue()
1245 : DateTimeService::getTimeZone()->getName()
1246 );
1247
1248 $providerContainer = new Collection();
1249
1250 if ($provider->getTimeZone()) {
1251 $this->providerService->modifyProviderTimeZone(
1252 $provider,
1253 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1254 $startDateTime,
1255 $endDateTime
1256 );
1257 }
1258
1259 $start = $provider->getTimeZone() ?
1260 DateTimeService::getCustomDateTimeObjectInTimeZone(
1261 $startDateTime->format('Y-m-d H:i'),
1262 $provider->getTimeZone()->getValue()
1263 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1264
1265 $end = $provider->getTimeZone() ?
1266 DateTimeService::getCustomDateTimeObjectInTimeZone(
1267 $endDateTime->format('Y-m-d H:i'),
1268 $provider->getTimeZone()->getValue()
1269 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1270
1271 $providerContainer->addItem($provider, $provider->getId()->getValue());
1272
1273 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1274 $settings['limitPerEmployee']['numberOfApp'] : null;
1275
1276 $freeIntervals = $this->getFreeTime(
1277 $service,
1278 $props['locationId'],
1279 $locations,
1280 $providerContainer,
1281 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1282 [] : $settings['globalDaysOff'],
1283 $start,
1284 $end,
1285 $props['personsCount'],
1286 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1287 $settings['allowBookingIfNotMin'],
1288 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1289 !empty($settings['allowAdminBookOverApp']),
1290 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount],
1291 !empty($settings['allowAdminBookAtAnyTime'])
1292 );
1293
1294 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1295 $service,
1296 $requiredTime,
1297 $freeIntervals,
1298 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1299 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1300 $settings['timeSlotLength'] ?: $requiredTime,
1301 $start,
1302 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1303 $settings['serviceDurationAsSlot'],
1304 $settings['bufferTimeInSlot'],
1305 $provider->getTimeZone() ?
1306 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName(),
1307 !empty($props['structured']),
1308 $customPricing
1309 );
1310 }
1311
1312 $freeSlots = [
1313 'available' => [],
1314 'occupied' => [],
1315 'appCount' => [],
1316 'duration' => $requiredTime / 60,
1317 ];
1318
1319 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1320 /** @var Provider $provider */
1321 $provider = $providers->getItem($providerKey);
1322
1323 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1324
1325 if (!empty($settings['allowAdminBookOverApp']) && !$props['isFrontEndBooking'] && $props['structured']) {
1326 $this->setBookedTimeSlots($providerSlots);
1327 }
1328
1329 foreach (['available', 'occupied'] as $type) {
1330 if ($provider->getTimeZone()) {
1331 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1332 $providerSlots[$type],
1333 $provider->getTimeZone()->getValue()
1334 );
1335 }
1336
1337 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1338 foreach ($dateSlots as $timeKey => $slotData) {
1339 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1340 $freeSlots[$type][$dateKey][$timeKey] = [];
1341 }
1342
1343 foreach ($slotData as $item) {
1344 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1345 }
1346
1347 if (isset($freeSlots[$type][$dateKey])) {
1348 if (!$freeSlots[$type][$dateKey]) {
1349 unset($freeSlots[$type][$dateKey]);
1350 } else {
1351 ksort($freeSlots[$type][$dateKey]);
1352 }
1353 }
1354 }
1355 }
1356 }
1357 }
1358
1359 return $freeSlots;
1360 }
1361
1362 /**
1363 * @param array $freeSlots
1364 *
1365 * @throws Exception
1366 */
1367 private function setBookedTimeSlots(&$freeSlots)
1368 {
1369 foreach (['available', 'occupied'] as $type) {
1370 foreach ($freeSlots[$type] as $dateString => $timeSlots) {
1371 foreach ($timeSlots as $timeString => $slots) {
1372 foreach ($slots as $slot) {
1373 if (isset($slot['d'])) {
1374 $appointmentStart = $this->intervalService->getSeconds($timeString . ':00') / 60;
1375
1376 $isSameDay = $appointmentStart + $slot['d'] <= 1440;
1377
1378 $appointmentEnd = $isSameDay
1379 ? $appointmentStart + $slot['d']
1380 : $appointmentStart + $slot['d'] - 1440;
1381
1382 if (isset($freeSlots['available'][$dateString])) {
1383 foreach ($freeSlots['available'][$dateString] as $inspectedTimeString => $inspectedSlotData) {
1384 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1385
1386 if (
1387 $inspectedSlot > $appointmentStart &&
1388 $inspectedSlot < ($isSameDay ? $appointmentEnd : 1440)
1389 ) {
1390 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1391 if ($slot['e'] === $inspectedSlot['e']) {
1392 $freeSlots['available'][$dateString][$inspectedTimeString][$inspectedIndex]['i'] = true;
1393
1394 break;
1395 }
1396 }
1397 }
1398 }
1399 }
1400
1401 if (!$isSameDay) {
1402 $nextDateString = (
1403 new \DateTime($dateString, DateTimeService::getTimeZone())
1404 )->modify('+1 day')->format('Y-m-d');
1405
1406 if (isset($freeSlots['available'][$nextDateString])) {
1407 foreach ($freeSlots['available'][$nextDateString] as $inspectedTimeString => $inspectedSlotData) {
1408 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1409
1410 if ($inspectedSlot < $appointmentEnd) {
1411 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1412 if ($slot['e'] === $inspectedSlot['e']) {
1413 $freeSlots['available'][$nextDateString][$inspectedTimeString][$inspectedIndex] = true;
1414 }
1415 }
1416 }
1417 }
1418 }
1419 }
1420 }
1421 }
1422 }
1423 }
1424 }
1425 }
1426 }
1427