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