PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
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 weeks ago
TimeSlotService.php
1573 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 += $specialDay['dates'];
168 }
169
170 $providerLocationId = $provider->getLocationId() ? $provider->getLocationId()->getValue() : null;
171 $providerTimeZone = $provider->getTimeZone() ? $provider->getTimeZone()->getValue() : null;
172
173 /** @var Appointment $app */
174 foreach ($provider->getAppointmentList()->getItems() as $app) {
175 $bookingStart = $app->getBookingStart()->getValue();
176 $bookingEnd = $app->getBookingEnd()->getValue();
177 $appServiceId = $app->getServiceId()->getValue();
178 $appLocationId = $app->getLocationId() ? $app->getLocationId()->getValue() : null;
179 $resolvedLocationId = $appLocationId !== null ? $appLocationId : $providerLocationId;
180 $bookingStartTimeKey = $bookingStart->format('H:i');
181 $bookingEndString = $bookingEnd->format('Y-m-d H:i:s');
182
183 $occupiedStart = $this->getProviderTimeZoneDateTime($bookingStart->format('Y-m-d H:i'), $providerTimeZone);
184
185 $occupiedEnd = $this->getProviderTimeZoneDateTime($bookingEnd->format('Y-m-d H:i'), $providerTimeZone);
186
187 if ($appServiceId) {
188 $occupiedStart->modify('-' . ($app->getService()->getTimeBefore() ? $app->getService()->getTimeBefore()->getValue() : 0) . ' seconds');
189
190 $occupiedEnd->modify('+' . ($app->getService()->getTimeAfter() ? $app->getService()->getTimeAfter()->getValue() : 0) . ' seconds');
191 }
192
193 $occupiedDateStart = $occupiedStart->format('Y-m-d');
194
195 $occupiedSecondsStart = $this->intervalService->getSeconds($occupiedStart->format('H:i') . ':00');
196
197 $occupiedSecondsEnd = $this->intervalService->getSeconds($occupiedEnd->format('H:i:s'));
198
199 if (
200 $occupiedDateStart === $occupiedEnd->format('Y-m-d') && !$bookOverApp
201 ) {
202 $intervals[$occupiedDateStart]['occupied'][$occupiedSecondsStart] = [
203 $occupiedSecondsStart,
204 $this->getModifiedEndInterval(
205 !array_key_exists($occupiedDateStart, $specialDays) ? $weekDaysIntervals : [],
206 $intervals,
207 $occupiedDateStart,
208 $occupiedSecondsStart,
209 $occupiedSecondsEnd
210 )
211 ];
212 } elseif (!$bookOverApp) {
213 $dates = $this->getPeriodDates($occupiedStart, $occupiedEnd);
214
215 $datesCount = sizeof($dates);
216
217 if ($datesCount === 1) {
218 $intervals[$dates[0]]['occupied'][$occupiedSecondsStart] = [
219 $occupiedSecondsStart,
220 $occupiedSecondsEnd === 0 ? 86400 : $occupiedSecondsEnd
221 ];
222 } else {
223 foreach ($dates as $index => $date) {
224 if ($index === 0) {
225 $intervals[$date]['occupied'][$occupiedSecondsStart] = [$occupiedSecondsStart, 86400];
226 } elseif ($index === $datesCount - 1) {
227 $modifiedEnd = $this->getModifiedEndInterval(
228 !array_key_exists($occupiedDateStart, $specialDays) ?
229 $weekDaysIntervals :
230 [],
231 $intervals,
232 $date,
233 0,
234 $occupiedSecondsEnd
235 );
236
237 $intervals[$date]['occupied'][0] = [
238 0,
239 $modifiedEnd === 0 ? 86400 : $modifiedEnd
240 ];
241 } else {
242 $intervals[$date]['occupied'][0] = [0, 86400];
243 }
244 }
245 }
246 }
247
248 if ($appServiceId === $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 $maxCapacity = $app->getService()->getMaxCapacity()->getValue();
264
265 $hasCapacity =
266 $personsCount !== null &&
267 ($persons + $personsCount) <= $maxCapacity &&
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 $durationMinutes = $this->getAppointmentDurationMinutes($bookingStart, $bookingEnd);
281
282 if (
283 ($hasLocation && $status === BookingStatus::APPROVED && $hasCapacity) ||
284 ($hasLocation && $status === BookingStatus::PENDING && ($bookIfPending || $hasCapacity))
285 ) {
286 $endDateTimeParts = explode(' ', $bookingEndString);
287
288 $intervals[$occupiedDateStart]['available'][$bookingStartTimeKey] =
289 [
290 'locationId' => $resolvedLocationId,
291 'places' => $maxCapacity - $persons,
292 'endDate' => $endDateTimeParts[0],
293 'endTime' => $endDateTimeParts[1],
294 'serviceId' => $serviceId,
295 'duration' => $durationMinutes,
296 ];
297 } else {
298 $this->setProviderFullInterval(
299 $intervals,
300 $occupiedDateStart,
301 $bookingStartTimeKey,
302 [
303 'locationId' => $resolvedLocationId,
304 'places' => $maxCapacity - $persons,
305 'end' => $bookingEndString,
306 'serviceId' => $appServiceId,
307 'duration' => $durationMinutes,
308 'waiting' => $personsWaiting,
309 ]
310 );
311 }
312 } else {
313 $this->setProviderFullInterval(
314 $intervals,
315 $occupiedDateStart,
316 $bookingStartTimeKey,
317 [
318 'locationId' => $resolvedLocationId,
319 'places' => 0,
320 'end' => $bookingEndString,
321 'serviceId' => $appServiceId,
322 'duration' => $this->getAppointmentDurationMinutes($bookingStart, $bookingEnd),
323 ]
324 );
325 }
326 }
327
328 return $intervals;
329 }
330
331 /**
332 * @param array $intervals
333 * @param string $dateStart
334 * @param string $timeKey
335 * @param array $entry
336 */
337 private function setProviderFullInterval(&$intervals, $dateStart, $timeKey, $entry)
338 {
339 if (
340 !isset($intervals[$dateStart]['full'][$timeKey]) ||
341 $entry['duration'] > $intervals[$dateStart]['full'][$timeKey]['duration']
342 ) {
343 $intervals[$dateStart]['full'][$timeKey] = $entry;
344 }
345 }
346
347 /**
348 * @param DateTime $bookingStart
349 * @param DateTime $bookingEnd
350 *
351 * @return int
352 */
353 private function getAppointmentDurationMinutes($bookingStart, $bookingEnd)
354 {
355 $duration = $bookingStart->diff($bookingEnd);
356
357 return ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i;
358 }
359
360 /**
361 * @param string $dateTimeString
362 * @param string|null $providerTimeZone
363 *
364 * @return DateTime
365 * @throws Exception
366 */
367 private function getProviderTimeZoneDateTime($dateTimeString, $providerTimeZone)
368 {
369 return $providerTimeZone
370 ? DateTimeService::getDateTimeObjectInTimeZone($dateTimeString, $providerTimeZone)
371 : DateTimeService::getCustomDateTimeObject($dateTimeString);
372 }
373
374 /**
375 * get provider day off dates.
376 *
377 * @param Provider $provider
378 *
379 * @return array
380 * @throws Exception
381 */
382 private function getProviderDayOffDates($provider)
383 {
384 $dates = [];
385
386 /** @var DayOff $dayOff */
387 foreach ($provider->getDayOffList()->getItems() as $dayOff) {
388 $endDateCopy = clone $dayOff->getEndDate()->getValue();
389
390 $dayOffPeriod = new DatePeriod(
391 $dayOff->getStartDate()->getValue(),
392 new DateInterval('P1D'),
393 $endDateCopy->modify('+1 day')
394 );
395
396 /** @var DateTime $date */
397 foreach ($dayOffPeriod as $date) {
398 $dateFormatted = $dayOff->getRepeat()->getValue() ?
399 $date->format('m-d') :
400 $date->format('Y-m-d');
401
402 $dates[$dateFormatted] = $dateFormatted;
403 }
404 }
405
406 return $dates;
407 }
408
409 /**
410 * get available appointment intervals.
411 *
412 * @param array $availableIntervals
413 * @param array $unavailableIntervals
414 *
415 * @return array
416 */
417 private function getAvailableIntervals(&$availableIntervals, $unavailableIntervals)
418 {
419 $parsedAvailablePeriod = [];
420
421 ksort($availableIntervals);
422 ksort($unavailableIntervals);
423
424 foreach ($availableIntervals as $available) {
425 $parsedAvailablePeriod[] = $available;
426
427 foreach ($unavailableIntervals as $unavailable) {
428 if ($parsedAvailablePeriod) {
429 $lastAvailablePeriod = $parsedAvailablePeriod[sizeof($parsedAvailablePeriod) - 1];
430
431 if ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
432 // unavailable interval is inside available interval
433 $fixedPeriod = array_pop($parsedAvailablePeriod);
434
435 if ($fixedPeriod[0] !== $unavailable[0]) {
436 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
437 }
438
439 if ($unavailable[1] !== $fixedPeriod[1]) {
440 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
441 }
442 } elseif (
443 $unavailable[0] <= $lastAvailablePeriod[0] &&
444 $unavailable[1] >= $lastAvailablePeriod[1]
445 ) {
446 // available interval is inside unavailable interval
447 array_pop($parsedAvailablePeriod);
448 } elseif (
449 $unavailable[0] <= $lastAvailablePeriod[0] &&
450 $unavailable[1] >= $lastAvailablePeriod[0] &&
451 $unavailable[1] <= $lastAvailablePeriod[1]
452 ) {
453 // unavailable interval intersect start of available interval
454 $fixedPeriod = array_pop($parsedAvailablePeriod);
455
456 if ($unavailable[1] !== $fixedPeriod[1]) {
457 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
458 }
459 } elseif (
460 $unavailable[0] >= $lastAvailablePeriod[0] &&
461 $unavailable[0] <= $lastAvailablePeriod[1] &&
462 $unavailable[1] >= $lastAvailablePeriod[1]
463 ) {
464 // unavailable interval intersect end of available interval
465 $fixedPeriod = array_pop($parsedAvailablePeriod);
466
467 if ($fixedPeriod[0] !== $unavailable[0]) {
468 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
469 }
470 }
471 }
472 }
473 }
474
475 return $parsedAvailablePeriod;
476 }
477
478 /**
479 * @param Service $service
480 * @param Provider $provider
481 * @param int $personsCount
482 *
483 * @return bool
484 *
485 * @throws Exception
486 */
487 private function getOnlyAppointmentsSlots($service, $provider, $personsCount)
488 {
489 $getOnlyAppointmentsSlots = false;
490
491 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
492 /** @var Service $providerService */
493 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
494
495 if ($personsCount < $providerService->getMinCapacity()->getValue()) {
496 $getOnlyAppointmentsSlots = true;
497 }
498 }
499
500 return $getOnlyAppointmentsSlots;
501 }
502
503 /** @noinspection MoreThanThreeArgumentsInspection */
504 /**
505 * @param Service $service
506 * @param int $locationId
507 * @param Collection $providers
508 * @param Collection $locations
509 * @param array $globalDaysOffDates
510 * @param DateTime $startDateTime
511 * @param DateTime $endDateTime
512 * @param int $personsCount
513 * @param boolean $bookIfPending
514 * @param boolean $bookIfNotMin
515 * @param boolean $bookAfterMin
516 * @param boolean $bookOverApp
517 * @param array $appointmentsCount
518 * @param boolean $allowAdminBookAtAnytime
519 *
520 * @return array
521 * @throws Exception
522 */
523 private function getFreeTime(
524 Service $service,
525 $locationId,
526 Collection $locations,
527 Collection $providers,
528 array $globalDaysOffDates,
529 DateTime $startDateTime,
530 DateTime $endDateTime,
531 $personsCount,
532 $bookIfPending,
533 $bookIfNotMin,
534 $bookAfterMin,
535 $bookOverApp,
536 $appointmentsCount,
537 $allowAdminBookAtAnytime
538 ) {
539
540 $weekDayIntervals = [];
541
542 $appointmentIntervals = [];
543
544 $daysOffDates = [];
545
546 $specialDayIntervals = [];
547
548 $getOnlyAppointmentsSlots = [];
549
550 $serviceId = $service->getId()->getValue();
551
552 /** @var Provider $provider */
553 foreach ($providers->getItems() as $provider) {
554 $providerId = $provider->getId()->getValue();
555
556 $getOnlyAppointmentsSlots[$providerId] = $bookIfNotMin && $bookAfterMin ? $this->getOnlyAppointmentsSlots(
557 $service,
558 $provider,
559 $personsCount
560 ) : false;
561
562 $daysOffDates[$providerId] = $this->getProviderDayOffDates($provider);
563
564 $weekDayIntervals[$providerId] = $this->scheduleService->getProviderWeekDaysIntervals(
565 $provider,
566 $locations,
567 $locationId,
568 $serviceId
569 );
570
571 $specialDayIntervals[$providerId] = $this->scheduleService->getProviderSpecialDayIntervals(
572 $provider,
573 $locations,
574 $locationId,
575 $serviceId
576 );
577
578 $appointmentIntervals[$providerId] = $this->getProviderAppointmentIntervals(
579 $provider,
580 $locations,
581 $serviceId,
582 $locationId,
583 $personsCount,
584 $bookIfPending,
585 $bookOverApp,
586 $weekDayIntervals[$providerId],
587 $specialDayIntervals[$providerId]
588 );
589 }
590
591 $freeDateIntervals = [];
592
593 foreach ($appointmentIntervals as $providerKey => $providerDates) {
594 foreach ((array)$providerDates as $dateKey => $dateIntervals) {
595 $dayIndex = DateTimeService::getDayIndex($dateKey);
596
597 $specialDayDateKey = null;
598
599 $emptySpecialDayKey = null;
600
601 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
602 if (array_key_exists($dateKey, $specialDays['dates']) && !empty($specialDays['intervals'])) {
603 $specialDayDateKey = $specialDayKey;
604 break;
605 } elseif (array_key_exists($dateKey, $specialDays['dates'])) {
606 $emptySpecialDayKey = $specialDayKey;
607 }
608 }
609
610 $specialDayDateKey = $specialDayDateKey !== null ? $specialDayDateKey : $emptySpecialDayKey;
611
612 if (
613 $specialDayDateKey !== null &&
614 isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])
615 ) {
616 // get free intervals if it is special day
617 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
618 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
619 !empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []
620 );
621 } elseif (
622 isset($weekDayIntervals[$providerKey][$dayIndex]['free']) &&
623 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
624 ) {
625 // get free intervals if it is working day
626 $unavailableIntervals =
627 $weekDayIntervals[$providerKey][$dayIndex]['busy'] + (!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []);
628
629 $intersectedTimes = array_intersect(
630 array_keys($weekDayIntervals[$providerKey][$dayIndex]['busy']),
631 array_keys(!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : [])
632 );
633
634 foreach ($intersectedTimes as $time) {
635 $unavailableIntervals[$time] =
636 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] >
637 $dateIntervals['occupied'][$time] ?
638 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] :
639 $dateIntervals['occupied'][$time];
640 }
641
642 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
643 $weekDayIntervals[$providerKey][$dayIndex]['free'],
644 $unavailableIntervals ?: []
645 );
646 }
647 }
648 }
649
650 $startDateTime = clone $startDateTime;
651
652 $startDateTime->setTime(0, 0);
653
654 $endDateTime = clone $endDateTime;
655
656 $endDateTime->modify('+1 day')->setTime(0, 0);
657
658 // create calendar
659 $period = new DatePeriod(
660 $startDateTime,
661 new DateInterval('P1D'),
662 $endDateTime
663 );
664
665 $calendar = [];
666
667 /** @var DateTime $day */
668 foreach ($period as $day) {
669 $currentDate = $day->format('Y-m-d');
670 $dayIndex = (int)$day->format('N');
671
672 $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) ||
673 array_key_exists($day->format('m-d'), $globalDaysOffDates);
674
675 if (!$isGlobalDayOff) {
676 foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) {
677 $isProviderDayOff = array_key_exists($currentDate, $daysOffDates[$providerKey]) ||
678 array_key_exists($day->format('m-d'), $daysOffDates[$providerKey]);
679
680 $specialDayDateKey = null;
681
682 $emptySpecialDayKey = null;
683
684 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
685 if (
686 array_key_exists($currentDate, $specialDays['dates']) &&
687 !empty($specialDays['intervals'])
688 ) {
689 $specialDayDateKey = $specialDayKey;
690 break;
691 } elseif (array_key_exists($currentDate, $specialDays['dates'])) {
692 $emptySpecialDayKey = $specialDayKey;
693 }
694 }
695
696 $specialDayDateKey = $specialDayDateKey !== null ? $specialDayDateKey : $emptySpecialDayKey;
697
698 if (!$isProviderDayOff) {
699 // daily limit per employee
700 if (
701 !$allowAdminBookAtAnytime &&
702 !empty($appointmentsCount['limitCount']) &&
703 !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
704 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']
705 ) {
706 continue;
707 }
708
709 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
710 // get date intervals if there are appointments (special or working day)
711 $calendar[$currentDate][$providerKey] = [
712 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
713 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
714 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
715 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
716 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
717 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
718 ];
719 } else {
720 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
721 // get date intervals if it is special day with out appointments
722 $calendar[$currentDate][$providerKey] = [
723 'slots' => [],
724 'full' => [],
725 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
726 [] :
727 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
728 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
729 $appointmentsCount[$providerKey][$currentDate] :
730 0
731 ];
732 } elseif (
733 isset($weekDayIntervals[$providerKey][$dayIndex]) &&
734 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
735 ) {
736 // get date intervals if it is working day without appointments
737 $calendar[$currentDate][$providerKey] = [
738 'slots' => [],
739 'full' => [],
740 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
741 [] :
742 $weekDayIntervals[$providerKey][$dayIndex]['free'],
743 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
744 $appointmentsCount[$providerKey][$currentDate] :
745 0
746 ];
747 }
748 }
749 }
750 }
751 }
752 }
753
754 return $calendar;
755 }
756
757 /** @noinspection MoreThanThreeArgumentsInspection */
758 /**
759 * @param Service $service
760 * @param int $requiredTime
761 * @param array $freeIntervals
762 * @param array $resourcedIntervals
763 * @param int $slotLength
764 * @param DateTime $startDateTime
765 * @param bool $serviceDurationAsSlot
766 * @param bool $bufferTimeInSlot
767 * @param String $timeZone
768 * @param bool $structured
769 * @param array $customPricing
770 *
771 * @return array
772 * @throws Exception
773 */
774 private function getAppointmentFreeSlots(
775 $service,
776 $requiredTime,
777 &$freeIntervals,
778 $resourcedIntervals,
779 $slotLength,
780 $startDateTime,
781 $serviceDurationAsSlot,
782 $bufferTimeInSlot,
783 $timeZone,
784 $structured,
785 $customPricing
786 ) {
787 $availableResult = [];
788
789 $occupiedResult = [];
790
791 $realRequiredTime = $requiredTime -
792 $service->getTimeBefore()->getValue() -
793 $service->getTimeAfter()->getValue();
794
795 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
796 $requiredTime = $requiredTime -
797 $service->getTimeBefore()->getValue() -
798 $service->getTimeAfter()->getValue();
799 }
800
801 $currentDateTime = DateTimeService::getNowDateTimeObject();
802
803 $currentDateString = $currentDateTime->format('Y-m-d');
804
805 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
806
807 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
808
809 $currentDateFormatted = $currentDateTime->format('Y-m-d');
810
811 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
812
813 $startDateFormatted = $startDateTime->format('Y-m-d');
814
815 $bookingLength = $serviceDurationAsSlot ? $requiredTime : $slotLength;
816
817 $appCount = [];
818
819 $isContinuousTime = false;
820
821 $continuousTimeSlot = null;
822
823 foreach ($freeIntervals as $dateKey => $dateProviders) {
824 foreach ((array)$dateProviders as $providerKey => $provider) {
825 foreach ((array)$provider['intervals'] as $timePeriod) {
826 $freeIntervalEnd = $timePeriod[1];
827
828 $moveStart = false;
829
830 if ($timePeriod[0] === 0 && $isContinuousTime && $continuousTimeSlot !== null) {
831 $isContinuousTime = false;
832
833 $moveStart = true;
834 }
835
836 if ($timePeriod[1] === 86400) {
837 $nextDate = DateTimeService::getDateTimeObjectInTimeZone(
838 $dateKey . ' 00:00:00',
839 $timeZone
840 )->modify('+1 days');
841
842 $nextDateString = $nextDate->format('Y-m-d');
843
844 if (
845 $nextDate->format('j') !== '1' &&
846 isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
847 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
848 ) {
849 $isContinuousTime = true;
850
851 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
852
853 $timePeriod[1] += (
854 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
855 ? $realRequiredTime + $service->getTimeAfter()->getValue()
856 : $nextDayInterval);
857
858 $freeIntervalEnd = $timePeriod[1];
859 }
860 }
861
862 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
863 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
864 }
865
866 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
867
868 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
869
870 $numberOfSlots = (int)(
871 floor(
872 (
873 $timePeriod[1] -
874 $providerTimeStart -
875 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
876 ) / $bookingLength
877 ) + 1
878 );
879
880 $inspectResourceIndexes = [];
881
882 if (isset($resourcedIntervals[$dateKey])) {
883 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
884 if (
885 array_intersect(
886 $timePeriod[2],
887 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
888 )
889 ) {
890 $inspectResourceIndexes[] = $resourceIndex;
891 }
892 }
893 }
894
895 $providerPeriodSlots = [];
896
897 $achievedLength = 0;
898
899 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
900 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
901
902 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
903
904 $numberOfSlots = (int)(
905 floor(
906 (
907 $timePeriod[1] -
908 $providerTimeStart -
909 ($requiredTime - $service->getTimeBefore()->getValue())
910 ) / $bookingLength
911 ) + 1
912 );
913 }
914
915 if ($moveStart) {
916 $continuousTimeSlot = null;
917 }
918
919 for ($i = 0; $i < $numberOfSlots; $i++) {
920 $achievedLength += $bookingLength;
921
922 $timeSlot = $customerTimeStart + $i * $bookingLength;
923
924 if (
925 $timeSlot + $realRequiredTime + $service->getTimeAfter()->getValue() <= $freeIntervalEnd &&
926 (
927 $startDateFormatted !== $dateKey || (
928 $startTimeInSeconds <= $timeSlot &&
929 (
930 $startDateFormatted !== $currentDateFormatted ||
931 $currentTimeInSeconds < $timeSlot
932 )
933 )
934 )
935 ) {
936 $timeSlotEnd = $timeSlot + $bookingLength;
937
938 $filteredLocationsIds = $timePeriod[2];
939
940 foreach ($inspectResourceIndexes as $resourceIndex) {
941 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
942 if (
943 ($start >= $timeSlot && $start < $timeSlotEnd) ||
944 ($end > $timeSlot && $end <= $timeSlotEnd) ||
945 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
946 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
947 ) {
948 $filteredLocationsIds = array_diff(
949 $filteredLocationsIds,
950 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
951 );
952
953 if (!$filteredLocationsIds) {
954 if ($achievedLength < $requiredTime) {
955 $providerPeriodSlots = [];
956
957 $achievedLength = 0;
958 }
959
960 continue 3;
961 }
962
963 $removedLocationsIds = array_diff(
964 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
965 $filteredLocationsIds
966 );
967
968 if ($removedLocationsIds && $achievedLength < $requiredTime) {
969 $parsedPeriodSlots = [];
970
971 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
972 if (
973 $start >= $previousTimeSlot &&
974 $start < $previousTimeSlot + $requiredTime
975 ) {
976 foreach ($periodSlotData as $data) {
977 if (!in_array($data[1], $removedLocationsIds)) {
978 $parsedPeriodSlots[$previousTimeSlot][] = $data;
979 }
980 }
981 } else {
982 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
983 }
984 }
985
986 $providerPeriodSlots = $parsedPeriodSlots;
987 }
988 }
989 }
990 }
991
992 if (!$timePeriod[2]) {
993 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
994 } elseif ($filteredLocationsIds) {
995 foreach ($filteredLocationsIds as $locationId) {
996 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
997 }
998 }
999 }
1000 }
1001
1002 foreach ($providerPeriodSlots as $timeSlot => $data) {
1003 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
1004 . sprintf('%02d', floor(($timeSlot / 60) % 60));
1005
1006 if ($timeSlot <= 86400) {
1007 if (!$structured && $time !== '24:00') {
1008 $availableResult[$dateKey][$time] = $data;
1009 } elseif ($time !== '24:00') {
1010 foreach ($data as $item) {
1011 $availableResult[$dateKey][$time][] = [
1012 'e' => $item[0],
1013 'l' => $item[1],
1014 'p' => $customPricing
1015 ? $this->providerService->getDateTimePrice(
1016 $customPricing,
1017 $dateKey,
1018 $timeSlot,
1019 $timeZone
1020 )
1021 : null,
1022 ];
1023 }
1024 }
1025
1026 if ($isContinuousTime) {
1027 $continuousTimeSlot = $timeSlot;
1028 }
1029 }
1030 }
1031 }
1032
1033 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
1034 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
1035
1036 if (
1037 $currentDateString === $dateKey &&
1038 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
1039 ) {
1040 continue;
1041 }
1042
1043 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
1044
1045 $newEndInSeconds = $startInSeconds + $realRequiredTime;
1046
1047 if (
1048 $newEndInSeconds !== 86400 &&
1049 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
1050 ) {
1051 if ($dateKey !== $appointmentData['endDate']) {
1052 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
1053 $dateKey . ' 00:00:00',
1054 $timeZone
1055 )->modify('+1 days')->format('Y-m-d');
1056
1057 if (
1058 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1059 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
1060 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1061 ) {
1062 continue;
1063 }
1064 } elseif ($newEndInSeconds > 86400) {
1065 $nextIntervalIsValid = false;
1066
1067 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1068 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
1069 $nextIntervalIsValid = true;
1070
1071 break;
1072 }
1073 }
1074
1075 if (!$nextIntervalIsValid) {
1076 continue;
1077 }
1078
1079 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
1080 $dateKey . ' 00:00:00',
1081 $timeZone
1082 )->modify('+1 days')->format('Y-m-d');
1083
1084 if (
1085 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1086 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
1087 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1088 ) {
1089 continue;
1090 }
1091 } else {
1092 $nextIntervalIsValid = false;
1093
1094 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1095 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
1096 $nextIntervalIsValid = true;
1097
1098 break;
1099 }
1100 }
1101
1102 if (!$nextIntervalIsValid) {
1103 continue;
1104 }
1105 }
1106 }
1107
1108 $availableResult[$dateKey][$appointmentTime] = [
1109 !$structured ? [
1110 $providerKey,
1111 $appointmentData['locationId'],
1112 $appointmentData['places'],
1113 $appointmentData['serviceId'],
1114 $appointmentData['duration'],
1115 ] : [
1116 'e' => $providerKey,
1117 'l' => $appointmentData['locationId'],
1118 'c' => $appointmentData['places'],
1119 's' => $appointmentData['serviceId'],
1120 'd' => $appointmentData['duration'],
1121 'p' => $customPricing
1122 ? $this->providerService->getDateTimePrice(
1123 $customPricing,
1124 $dateKey,
1125 $this->intervalService->getSeconds($appointmentTime),
1126 $timeZone
1127 )
1128 : null,
1129 ]
1130 ];
1131 }
1132
1133 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
1134 $occupiedResult[$dateKey][$appointmentTime][] = !$structured ? [
1135 $providerKey,
1136 $appointmentData['locationId'],
1137 $appointmentData['places'],
1138 $appointmentData['serviceId'],
1139 $appointmentData['duration'],
1140 ] : [
1141 'e' => $providerKey,
1142 'l' => $appointmentData['locationId'],
1143 'c' => $appointmentData['places'],
1144 's' => $appointmentData['serviceId'],
1145 'd' => $appointmentData['duration'],
1146 'w' => $appointmentData['waiting'] ?? 0,
1147 ];
1148 }
1149
1150 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
1151 }
1152 }
1153
1154 return [
1155 'available' => $availableResult,
1156 'occupied' => $occupiedResult,
1157 'appCount' => $appCount
1158 ];
1159 }
1160
1161 /**
1162 * @param array $slots
1163 * @param string $timeZone
1164 *
1165 * @return array
1166 * @throws Exception
1167 */
1168 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
1169 {
1170 $convertedProviderSlots = [];
1171
1172 foreach ($slots as $slotDate => $slotTimes) {
1173 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
1174 $convertedSlotParts = explode(
1175 ' ',
1176 DateTimeService::getDateTimeObjectInTimeZone(
1177 $slotDate . ' ' . $slotTime,
1178 $timeZone
1179 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
1180 );
1181
1182 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1183 }
1184 }
1185
1186 return $convertedProviderSlots;
1187 }
1188
1189
1190 /**
1191 * @param Collection $appointments
1192 * @param int $excludeAppointmentId
1193 *
1194 * @return array
1195 * @throws Exception
1196 */
1197 public function getAppointmentCount($appointments, $excludeAppointmentId)
1198 {
1199 $appCount = [];
1200
1201 /** @var Appointment $appointment */
1202 foreach ($appointments->getItems() as $appointment) {
1203 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1204 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1205 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1206 } else {
1207 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1208 }
1209 }
1210 }
1211
1212 return $appCount;
1213 }
1214
1215 /** @noinspection MoreThanThreeArgumentsInspection */
1216 /**
1217 * @param array $settings
1218 * @param array $props
1219 * @param SlotsEntities $slotsEntities
1220 * @param Collection $appointments
1221 *
1222 * @return array
1223 * @throws Exception
1224 */
1225 public function getSlots($settings, $props, $slotsEntities, $appointments)
1226 {
1227 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1228
1229 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1230 $this->resourceService->manageResources(
1231 $slotsEntities->getResources(),
1232 $appointments,
1233 $slotsEntities->getLocations(),
1234 $slotsEntities->getServices()->getItem($props['serviceId']),
1235 $slotsEntities->getProviders(),
1236 $props['locationId'],
1237 $props['excludeAppointmentId'],
1238 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1239 ) : [];
1240
1241 $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1242
1243 $this->providerService->addAppointmentsToAppointmentList(
1244 $slotsEntities->getProviders(),
1245 $appointments,
1246 $settings['isGloballyBusySlot']
1247 );
1248
1249 return $this->getCalculatedFreeSlots(
1250 $settings,
1251 $props,
1252 $slotsEntities,
1253 $resourcedLocationsIntervals,
1254 $appointmentsCount,
1255 $settings['normalProvidersIntervals'] ?? []
1256 );
1257 }
1258
1259 /** @noinspection MoreThanThreeArgumentsInspection */
1260 /**
1261 * @param array $settings
1262 * @param array $props
1263 * @param SlotsEntities $slotsEntities
1264 * @param array $resourcedLocationsIntervals
1265 * @param array $appointmentsCount
1266 * @param array $normalProvidersIntervals
1267 *
1268 * @return array
1269 * @throws Exception
1270 */
1271 private function getCalculatedFreeSlots(
1272 $settings,
1273 $props,
1274 $slotsEntities,
1275 $resourcedLocationsIntervals,
1276 $appointmentsCount,
1277 $normalProvidersIntervals = []
1278 ) {
1279 $freeProvidersSlots = [];
1280
1281 /** @var DateTime $startDateTime */
1282 $startDateTime = $props['startDateTime'];
1283
1284 /** @var DateTime $endDateTime */
1285 $endDateTime = $props['endDateTime'];
1286
1287 /** @var Service $service */
1288 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1289
1290 /** @var Collection $providers */
1291 $providers = $slotsEntities->getProviders();
1292
1293 /** @var Collection $locations */
1294 $locations = $slotsEntities->getLocations();
1295
1296 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1297 $service,
1298 $props['extras']
1299 );
1300
1301 /** @var Provider $provider */
1302 foreach ($providers->getItems() as $provider) {
1303 /** @var Service $providerService */
1304 $providerService = $service;
1305
1306 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1307 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1308
1309 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1310 continue;
1311 }
1312 }
1313
1314 $customPricing = $this->providerService->getCustomPricing(
1315 $providerService,
1316 $provider->getTimeZone()
1317 ? $provider->getTimeZone()->getValue()
1318 : DateTimeService::getTimeZone()->getName()
1319 );
1320
1321 $providerContainer = new Collection();
1322
1323 if ($provider->getTimeZone()) {
1324 $this->providerService->modifyProviderTimeZone(
1325 $provider,
1326 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1327 $startDateTime,
1328 $endDateTime
1329 );
1330 }
1331
1332 $start = $provider->getTimeZone() ?
1333 DateTimeService::getCustomDateTimeObjectInTimeZone(
1334 $startDateTime->format('Y-m-d H:i'),
1335 $provider->getTimeZone()->getValue()
1336 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1337
1338 $end = $provider->getTimeZone() ?
1339 DateTimeService::getCustomDateTimeObjectInTimeZone(
1340 $endDateTime->format('Y-m-d H:i'),
1341 $provider->getTimeZone()->getValue()
1342 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1343
1344 $providerContainer->addItem($provider, $provider->getId()->getValue());
1345
1346 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1347 $settings['limitPerEmployee']['numberOfApp'] : null;
1348
1349 $freeIntervals = $this->getFreeTime(
1350 $service,
1351 $props['locationId'],
1352 $locations,
1353 $providerContainer,
1354 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1355 [] : $settings['globalDaysOff'],
1356 $start,
1357 $end,
1358 $props['personsCount'],
1359 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1360 $settings['allowBookingIfNotMin'],
1361 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1362 !empty($settings['allowAdminBookOverApp']),
1363 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount],
1364 !empty($settings['allowAdminBookAtAnyTime'])
1365 );
1366
1367 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1368 $service,
1369 $requiredTime,
1370 $freeIntervals,
1371 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1372 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1373 $settings['timeSlotLength'] ?: $requiredTime,
1374 $start,
1375 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1376 $settings['serviceDurationAsSlot'],
1377 $settings['bufferTimeInSlot'],
1378 $provider->getTimeZone() ?
1379 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName(),
1380 !empty($props['structured']),
1381 $customPricing
1382 );
1383 }
1384
1385 $freeSlots = [
1386 'available' => [],
1387 'occupied' => [],
1388 'appCount' => [],
1389 'duration' => $requiredTime / 60,
1390 ];
1391
1392 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1393 /** @var Provider $provider */
1394 $provider = $providers->getItem($providerKey);
1395
1396 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1397
1398 if (!empty($settings['allowAdminBookOverApp']) && !$props['isFrontEndBooking'] && !empty($props['structured'])) {
1399 $this->setBookedTimeSlots($providerSlots);
1400 }
1401
1402 // Mark slots outside the employee's normal working hours when allowAdminBookAtAnyTime is enabled
1403 if (!empty($settings['allowAdminBookAtAnyTime']) && !empty($props['structured']) && isset($normalProvidersIntervals[$providerKey])) {
1404 $weekDayIntervals = $normalProvidersIntervals[$providerKey]['weekDays'] ?? [];
1405 $specialDayIntervals = $normalProvidersIntervals[$providerKey]['specialDays'] ?? [];
1406
1407 foreach ($providerSlots['available'] as $dateKey => &$timeSlots) {
1408 foreach ($timeSlots as $timeKey => &$slotData) {
1409 $timeInSeconds = $this->intervalService->getSeconds($timeKey . ':00');
1410
1411 if (!$this->isTimeInWorkingHours($dateKey, $timeInSeconds, $weekDayIntervals, $specialDayIntervals)) {
1412 foreach ($slotData as &$slot) {
1413 if (is_array($slot)) {
1414 $slot['a'] = true;
1415 }
1416 }
1417 unset($slot);
1418 }
1419 }
1420 unset($slotData);
1421 }
1422 unset($timeSlots);
1423 }
1424
1425 foreach (['available', 'occupied'] as $type) {
1426 if ($provider->getTimeZone()) {
1427 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1428 $providerSlots[$type],
1429 $provider->getTimeZone()->getValue()
1430 );
1431 }
1432
1433 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1434 foreach ($dateSlots as $timeKey => $slotData) {
1435 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1436 $freeSlots[$type][$dateKey][$timeKey] = [];
1437 }
1438
1439 foreach ($slotData as $item) {
1440 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1441 }
1442
1443 if (isset($freeSlots[$type][$dateKey])) {
1444 if (!$freeSlots[$type][$dateKey]) {
1445 unset($freeSlots[$type][$dateKey]);
1446 } else {
1447 ksort($freeSlots[$type][$dateKey]);
1448 }
1449 }
1450 }
1451 }
1452 }
1453 }
1454
1455 return $freeSlots;
1456 }
1457
1458 /**
1459 * Determine whether a given time falls within the provider's normal working hours for a date.
1460 *
1461 * @param string $dateKey e.g. "2024-03-20"
1462 * @param int $timeInSeconds seconds since midnight (e.g. 32400 for 09:00)
1463 * @param array $weekDayIntervals keyed by day-of-week index (1=Mon … 7=Sun)
1464 * @param array $specialDayIntervals
1465 *
1466 * @return bool
1467 */
1468 private function isTimeInWorkingHours($dateKey, $timeInSeconds, $weekDayIntervals, $specialDayIntervals)
1469 {
1470 $specialDayMatched = false;
1471
1472 foreach ($specialDayIntervals as $specialDay) {
1473 if (array_key_exists($dateKey, $specialDay['dates'])) {
1474 $specialDayMatched = true;
1475
1476 if (empty($specialDay['intervals']['free'])) {
1477 continue;
1478 }
1479
1480 foreach ($specialDay['intervals']['free'] as $interval) {
1481 if ($timeInSeconds >= $interval[0] && $timeInSeconds < $interval[1]) {
1482 return true;
1483 }
1484 }
1485 }
1486 }
1487
1488 // If we found a matching special day, return false (time not in any matching interval)
1489 if ($specialDayMatched) {
1490 return false;
1491 }
1492
1493 $dayIndex = DateTimeService::getDayIndex($dateKey);
1494
1495 if (empty($weekDayIntervals[$dayIndex]['free'])) {
1496 return false;
1497 }
1498
1499 foreach ($weekDayIntervals[$dayIndex]['free'] as $interval) {
1500 if ($timeInSeconds >= $interval[0] && $timeInSeconds < $interval[1]) {
1501 return true;
1502 }
1503 }
1504
1505 return false;
1506 }
1507
1508 /**
1509 * @param array $freeSlots
1510 *
1511 * @throws Exception
1512 */
1513 private function setBookedTimeSlots(&$freeSlots)
1514 {
1515 foreach (['available', 'occupied'] as $type) {
1516 foreach ($freeSlots[$type] as $dateString => $timeSlots) {
1517 foreach ($timeSlots as $timeString => $slots) {
1518 foreach ($slots as $slot) {
1519 if (isset($slot['d'])) {
1520 $appointmentStart = $this->intervalService->getSeconds($timeString . ':00') / 60;
1521
1522 $isSameDay = $appointmentStart + $slot['d'] <= 1440;
1523
1524 $appointmentEnd = $isSameDay
1525 ? $appointmentStart + $slot['d']
1526 : $appointmentStart + $slot['d'] - 1440;
1527
1528 if (isset($freeSlots['available'][$dateString])) {
1529 foreach ($freeSlots['available'][$dateString] as $inspectedTimeString => $inspectedSlotData) {
1530 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1531
1532 if (
1533 $inspectedSlot > $appointmentStart &&
1534 $inspectedSlot < ($isSameDay ? $appointmentEnd : 1440)
1535 ) {
1536 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1537 if ($slot['e'] === $inspectedSlot['e']) {
1538 $freeSlots['available'][$dateString][$inspectedTimeString][$inspectedIndex]['i'] = true;
1539
1540 break;
1541 }
1542 }
1543 }
1544 }
1545 }
1546
1547 if (!$isSameDay) {
1548 $nextDateString = (
1549 new \DateTime($dateString, DateTimeService::getTimeZone())
1550 )->modify('+1 day')->format('Y-m-d');
1551
1552 if (isset($freeSlots['available'][$nextDateString])) {
1553 foreach ($freeSlots['available'][$nextDateString] as $inspectedTimeString => $inspectedSlotData) {
1554 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1555
1556 if ($inspectedSlot < $appointmentEnd) {
1557 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1558 if ($slot['e'] === $inspectedSlot['e']) {
1559 $freeSlots['available'][$nextDateString][$inspectedTimeString][$inspectedIndex] = true;
1560 }
1561 }
1562 }
1563 }
1564 }
1565 }
1566 }
1567 }
1568 }
1569 }
1570 }
1571 }
1572 }
1573