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