PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.8
Booking for Appointments and Events Calendar – Amelia v2.4.8
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 3 days ago
TimeSlotService.php
1576 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 isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
846 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
847 ) {
848 $isContinuousTime = true;
849
850 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
851
852 $timePeriod[1] += (
853 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
854 ? $realRequiredTime + $service->getTimeAfter()->getValue()
855 : $nextDayInterval);
856
857 $freeIntervalEnd = $timePeriod[1];
858 }
859
860 if ($nextDate->format('j') === '1') {
861 $isContinuousTime = false;
862 }
863 }
864
865 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
866 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
867 }
868
869 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
870
871 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
872
873 $numberOfSlots = (int)(
874 floor(
875 (
876 $timePeriod[1] -
877 $providerTimeStart -
878 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
879 ) / $bookingLength
880 ) + 1
881 );
882
883 $inspectResourceIndexes = [];
884
885 if (isset($resourcedIntervals[$dateKey])) {
886 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
887 if (
888 array_intersect(
889 $timePeriod[2],
890 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
891 )
892 ) {
893 $inspectResourceIndexes[] = $resourceIndex;
894 }
895 }
896 }
897
898 $providerPeriodSlots = [];
899
900 $achievedLength = 0;
901
902 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
903 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
904
905 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
906
907 $numberOfSlots = (int)(
908 floor(
909 (
910 $timePeriod[1] -
911 $providerTimeStart -
912 ($requiredTime - $service->getTimeBefore()->getValue())
913 ) / $bookingLength
914 ) + 1
915 );
916 }
917
918 if ($moveStart) {
919 $continuousTimeSlot = null;
920 }
921
922 for ($i = 0; $i < $numberOfSlots; $i++) {
923 $achievedLength += $bookingLength;
924
925 $timeSlot = $customerTimeStart + $i * $bookingLength;
926
927 if (
928 $timeSlot + $realRequiredTime + $service->getTimeAfter()->getValue() <= $freeIntervalEnd &&
929 (
930 $startDateFormatted !== $dateKey || (
931 $startTimeInSeconds <= $timeSlot &&
932 (
933 $startDateFormatted !== $currentDateFormatted ||
934 $currentTimeInSeconds < $timeSlot
935 )
936 )
937 )
938 ) {
939 $timeSlotEnd = $timeSlot + $bookingLength;
940
941 $filteredLocationsIds = $timePeriod[2];
942
943 foreach ($inspectResourceIndexes as $resourceIndex) {
944 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
945 if (
946 ($start >= $timeSlot && $start < $timeSlotEnd) ||
947 ($end > $timeSlot && $end <= $timeSlotEnd) ||
948 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
949 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
950 ) {
951 $filteredLocationsIds = array_diff(
952 $filteredLocationsIds,
953 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
954 );
955
956 if (!$filteredLocationsIds) {
957 if ($achievedLength < $requiredTime) {
958 $providerPeriodSlots = [];
959
960 $achievedLength = 0;
961 }
962
963 continue 3;
964 }
965
966 $removedLocationsIds = array_diff(
967 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
968 $filteredLocationsIds
969 );
970
971 if ($removedLocationsIds && $achievedLength < $requiredTime) {
972 $parsedPeriodSlots = [];
973
974 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
975 if (
976 $start >= $previousTimeSlot &&
977 $start < $previousTimeSlot + $requiredTime
978 ) {
979 foreach ($periodSlotData as $data) {
980 if (!in_array($data[1], $removedLocationsIds)) {
981 $parsedPeriodSlots[$previousTimeSlot][] = $data;
982 }
983 }
984 } else {
985 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
986 }
987 }
988
989 $providerPeriodSlots = $parsedPeriodSlots;
990 }
991 }
992 }
993 }
994
995 if (!$timePeriod[2]) {
996 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
997 } elseif ($filteredLocationsIds) {
998 foreach ($filteredLocationsIds as $locationId) {
999 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
1000 }
1001 }
1002 }
1003 }
1004
1005 foreach ($providerPeriodSlots as $timeSlot => $data) {
1006 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
1007 . sprintf('%02d', floor(($timeSlot / 60) % 60));
1008
1009 if ($timeSlot <= 86400) {
1010 if (!$structured && $time !== '24:00') {
1011 $availableResult[$dateKey][$time] = $data;
1012 } elseif ($time !== '24:00') {
1013 foreach ($data as $item) {
1014 $availableResult[$dateKey][$time][] = [
1015 'e' => $item[0],
1016 'l' => $item[1],
1017 'p' => $customPricing
1018 ? $this->providerService->getDateTimePrice(
1019 $customPricing,
1020 $dateKey,
1021 $timeSlot,
1022 $timeZone
1023 )
1024 : null,
1025 ];
1026 }
1027 }
1028
1029 if ($isContinuousTime) {
1030 $continuousTimeSlot = $timeSlot;
1031 }
1032 }
1033 }
1034 }
1035
1036 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
1037 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
1038
1039 if (
1040 $currentDateString === $dateKey &&
1041 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
1042 ) {
1043 continue;
1044 }
1045
1046 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
1047
1048 $newEndInSeconds = $startInSeconds + $realRequiredTime;
1049
1050 if (
1051 $newEndInSeconds !== 86400 &&
1052 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
1053 ) {
1054 if ($dateKey !== $appointmentData['endDate']) {
1055 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
1056 $dateKey . ' 00:00:00',
1057 $timeZone
1058 )->modify('+1 days')->format('Y-m-d');
1059
1060 if (
1061 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1062 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
1063 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1064 ) {
1065 continue;
1066 }
1067 } elseif ($newEndInSeconds > 86400) {
1068 $nextIntervalIsValid = false;
1069
1070 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1071 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
1072 $nextIntervalIsValid = true;
1073
1074 break;
1075 }
1076 }
1077
1078 if (!$nextIntervalIsValid) {
1079 continue;
1080 }
1081
1082 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
1083 $dateKey . ' 00:00:00',
1084 $timeZone
1085 )->modify('+1 days')->format('Y-m-d');
1086
1087 if (
1088 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
1089 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
1090 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
1091 ) {
1092 continue;
1093 }
1094 } else {
1095 $nextIntervalIsValid = false;
1096
1097 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
1098 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
1099 $nextIntervalIsValid = true;
1100
1101 break;
1102 }
1103 }
1104
1105 if (!$nextIntervalIsValid) {
1106 continue;
1107 }
1108 }
1109 }
1110
1111 $availableResult[$dateKey][$appointmentTime] = [
1112 !$structured ? [
1113 $providerKey,
1114 $appointmentData['locationId'],
1115 $appointmentData['places'],
1116 $appointmentData['serviceId'],
1117 $appointmentData['duration'],
1118 ] : [
1119 'e' => $providerKey,
1120 'l' => $appointmentData['locationId'],
1121 'c' => $appointmentData['places'],
1122 's' => $appointmentData['serviceId'],
1123 'd' => $appointmentData['duration'],
1124 'p' => $customPricing
1125 ? $this->providerService->getDateTimePrice(
1126 $customPricing,
1127 $dateKey,
1128 $this->intervalService->getSeconds($appointmentTime),
1129 $timeZone
1130 )
1131 : null,
1132 ]
1133 ];
1134 }
1135
1136 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
1137 $occupiedResult[$dateKey][$appointmentTime][] = !$structured ? [
1138 $providerKey,
1139 $appointmentData['locationId'],
1140 $appointmentData['places'],
1141 $appointmentData['serviceId'],
1142 $appointmentData['duration'],
1143 ] : [
1144 'e' => $providerKey,
1145 'l' => $appointmentData['locationId'],
1146 'c' => $appointmentData['places'],
1147 's' => $appointmentData['serviceId'],
1148 'd' => $appointmentData['duration'],
1149 'w' => $appointmentData['waiting'] ?? 0,
1150 ];
1151 }
1152
1153 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
1154 }
1155 }
1156
1157 return [
1158 'available' => $availableResult,
1159 'occupied' => $occupiedResult,
1160 'appCount' => $appCount
1161 ];
1162 }
1163
1164 /**
1165 * @param array $slots
1166 * @param string $timeZone
1167 *
1168 * @return array
1169 * @throws Exception
1170 */
1171 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
1172 {
1173 $convertedProviderSlots = [];
1174
1175 foreach ($slots as $slotDate => $slotTimes) {
1176 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
1177 $convertedSlotParts = explode(
1178 ' ',
1179 DateTimeService::getDateTimeObjectInTimeZone(
1180 $slotDate . ' ' . $slotTime,
1181 $timeZone
1182 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
1183 );
1184
1185 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1186 }
1187 }
1188
1189 return $convertedProviderSlots;
1190 }
1191
1192
1193 /**
1194 * @param Collection $appointments
1195 * @param int $excludeAppointmentId
1196 *
1197 * @return array
1198 * @throws Exception
1199 */
1200 public function getAppointmentCount($appointments, $excludeAppointmentId)
1201 {
1202 $appCount = [];
1203
1204 /** @var Appointment $appointment */
1205 foreach ($appointments->getItems() as $appointment) {
1206 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1207 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1208 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1209 } else {
1210 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1211 }
1212 }
1213 }
1214
1215 return $appCount;
1216 }
1217
1218 /** @noinspection MoreThanThreeArgumentsInspection */
1219 /**
1220 * @param array $settings
1221 * @param array $props
1222 * @param SlotsEntities $slotsEntities
1223 * @param Collection $appointments
1224 *
1225 * @return array
1226 * @throws Exception
1227 */
1228 public function getSlots($settings, $props, $slotsEntities, $appointments)
1229 {
1230 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1231
1232 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1233 $this->resourceService->manageResources(
1234 $slotsEntities->getResources(),
1235 $appointments,
1236 $slotsEntities->getLocations(),
1237 $slotsEntities->getServices()->getItem($props['serviceId']),
1238 $slotsEntities->getProviders(),
1239 $props['locationId'],
1240 $props['excludeAppointmentId'],
1241 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1242 ) : [];
1243
1244 $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1245
1246 $this->providerService->addAppointmentsToAppointmentList(
1247 $slotsEntities->getProviders(),
1248 $appointments,
1249 $settings['isGloballyBusySlot']
1250 );
1251
1252 return $this->getCalculatedFreeSlots(
1253 $settings,
1254 $props,
1255 $slotsEntities,
1256 $resourcedLocationsIntervals,
1257 $appointmentsCount,
1258 $settings['normalProvidersIntervals'] ?? []
1259 );
1260 }
1261
1262 /** @noinspection MoreThanThreeArgumentsInspection */
1263 /**
1264 * @param array $settings
1265 * @param array $props
1266 * @param SlotsEntities $slotsEntities
1267 * @param array $resourcedLocationsIntervals
1268 * @param array $appointmentsCount
1269 * @param array $normalProvidersIntervals
1270 *
1271 * @return array
1272 * @throws Exception
1273 */
1274 private function getCalculatedFreeSlots(
1275 $settings,
1276 $props,
1277 $slotsEntities,
1278 $resourcedLocationsIntervals,
1279 $appointmentsCount,
1280 $normalProvidersIntervals = []
1281 ) {
1282 $freeProvidersSlots = [];
1283
1284 /** @var DateTime $startDateTime */
1285 $startDateTime = $props['startDateTime'];
1286
1287 /** @var DateTime $endDateTime */
1288 $endDateTime = $props['endDateTime'];
1289
1290 /** @var Service $service */
1291 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1292
1293 /** @var Collection $providers */
1294 $providers = $slotsEntities->getProviders();
1295
1296 /** @var Collection $locations */
1297 $locations = $slotsEntities->getLocations();
1298
1299 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1300 $service,
1301 $props['extras']
1302 );
1303
1304 /** @var Provider $provider */
1305 foreach ($providers->getItems() as $provider) {
1306 /** @var Service $providerService */
1307 $providerService = $service;
1308
1309 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1310 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1311
1312 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1313 continue;
1314 }
1315 }
1316
1317 $customPricing = $this->providerService->getCustomPricing(
1318 $providerService,
1319 $provider->getTimeZone()
1320 ? $provider->getTimeZone()->getValue()
1321 : DateTimeService::getTimeZone()->getName()
1322 );
1323
1324 $providerContainer = new Collection();
1325
1326 if ($provider->getTimeZone()) {
1327 $this->providerService->modifyProviderTimeZone(
1328 $provider,
1329 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1330 $startDateTime,
1331 $endDateTime
1332 );
1333 }
1334
1335 $start = $provider->getTimeZone() ?
1336 DateTimeService::getCustomDateTimeObjectInTimeZone(
1337 $startDateTime->format('Y-m-d H:i'),
1338 $provider->getTimeZone()->getValue()
1339 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1340
1341 $end = $provider->getTimeZone() ?
1342 DateTimeService::getCustomDateTimeObjectInTimeZone(
1343 $endDateTime->format('Y-m-d H:i'),
1344 $provider->getTimeZone()->getValue()
1345 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1346
1347 $providerContainer->addItem($provider, $provider->getId()->getValue());
1348
1349 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1350 $settings['limitPerEmployee']['numberOfApp'] : null;
1351
1352 $freeIntervals = $this->getFreeTime(
1353 $service,
1354 $props['locationId'],
1355 $locations,
1356 $providerContainer,
1357 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1358 [] : $settings['globalDaysOff'],
1359 $start,
1360 $end,
1361 $props['personsCount'],
1362 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1363 $settings['allowBookingIfNotMin'],
1364 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1365 !empty($settings['allowAdminBookOverApp']),
1366 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount],
1367 !empty($settings['allowAdminBookAtAnyTime'])
1368 );
1369
1370 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1371 $service,
1372 $requiredTime,
1373 $freeIntervals,
1374 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1375 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1376 $settings['timeSlotLength'] ?: $requiredTime,
1377 $start,
1378 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1379 $settings['serviceDurationAsSlot'],
1380 $settings['bufferTimeInSlot'],
1381 $provider->getTimeZone() ?
1382 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName(),
1383 !empty($props['structured']),
1384 $customPricing
1385 );
1386 }
1387
1388 $freeSlots = [
1389 'available' => [],
1390 'occupied' => [],
1391 'appCount' => [],
1392 'duration' => $requiredTime / 60,
1393 ];
1394
1395 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1396 /** @var Provider $provider */
1397 $provider = $providers->getItem($providerKey);
1398
1399 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1400
1401 if (!empty($settings['allowAdminBookOverApp']) && !$props['isFrontEndBooking'] && !empty($props['structured'])) {
1402 $this->setBookedTimeSlots($providerSlots);
1403 }
1404
1405 // Mark slots outside the employee's normal working hours when allowAdminBookAtAnyTime is enabled
1406 if (!empty($settings['allowAdminBookAtAnyTime']) && !empty($props['structured']) && isset($normalProvidersIntervals[$providerKey])) {
1407 $weekDayIntervals = $normalProvidersIntervals[$providerKey]['weekDays'] ?? [];
1408 $specialDayIntervals = $normalProvidersIntervals[$providerKey]['specialDays'] ?? [];
1409
1410 foreach ($providerSlots['available'] as $dateKey => &$timeSlots) {
1411 foreach ($timeSlots as $timeKey => &$slotData) {
1412 $timeInSeconds = $this->intervalService->getSeconds($timeKey . ':00');
1413
1414 if (!$this->isTimeInWorkingHours($dateKey, $timeInSeconds, $weekDayIntervals, $specialDayIntervals)) {
1415 foreach ($slotData as &$slot) {
1416 if (is_array($slot)) {
1417 $slot['a'] = true;
1418 }
1419 }
1420 unset($slot);
1421 }
1422 }
1423 unset($slotData);
1424 }
1425 unset($timeSlots);
1426 }
1427
1428 foreach (['available', 'occupied'] as $type) {
1429 if ($provider->getTimeZone()) {
1430 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1431 $providerSlots[$type],
1432 $provider->getTimeZone()->getValue()
1433 );
1434 }
1435
1436 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1437 foreach ($dateSlots as $timeKey => $slotData) {
1438 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1439 $freeSlots[$type][$dateKey][$timeKey] = [];
1440 }
1441
1442 foreach ($slotData as $item) {
1443 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1444 }
1445
1446 if (isset($freeSlots[$type][$dateKey])) {
1447 if (!$freeSlots[$type][$dateKey]) {
1448 unset($freeSlots[$type][$dateKey]);
1449 } else {
1450 ksort($freeSlots[$type][$dateKey]);
1451 }
1452 }
1453 }
1454 }
1455 }
1456 }
1457
1458 return $freeSlots;
1459 }
1460
1461 /**
1462 * Determine whether a given time falls within the provider's normal working hours for a date.
1463 *
1464 * @param string $dateKey e.g. "2024-03-20"
1465 * @param int $timeInSeconds seconds since midnight (e.g. 32400 for 09:00)
1466 * @param array $weekDayIntervals keyed by day-of-week index (1=Mon … 7=Sun)
1467 * @param array $specialDayIntervals
1468 *
1469 * @return bool
1470 */
1471 private function isTimeInWorkingHours($dateKey, $timeInSeconds, $weekDayIntervals, $specialDayIntervals)
1472 {
1473 $specialDayMatched = false;
1474
1475 foreach ($specialDayIntervals as $specialDay) {
1476 if (array_key_exists($dateKey, $specialDay['dates'])) {
1477 $specialDayMatched = true;
1478
1479 if (empty($specialDay['intervals']['free'])) {
1480 continue;
1481 }
1482
1483 foreach ($specialDay['intervals']['free'] as $interval) {
1484 if ($timeInSeconds >= $interval[0] && $timeInSeconds < $interval[1]) {
1485 return true;
1486 }
1487 }
1488 }
1489 }
1490
1491 // If we found a matching special day, return false (time not in any matching interval)
1492 if ($specialDayMatched) {
1493 return false;
1494 }
1495
1496 $dayIndex = DateTimeService::getDayIndex($dateKey);
1497
1498 if (empty($weekDayIntervals[$dayIndex]['free'])) {
1499 return false;
1500 }
1501
1502 foreach ($weekDayIntervals[$dayIndex]['free'] as $interval) {
1503 if ($timeInSeconds >= $interval[0] && $timeInSeconds < $interval[1]) {
1504 return true;
1505 }
1506 }
1507
1508 return false;
1509 }
1510
1511 /**
1512 * @param array $freeSlots
1513 *
1514 * @throws Exception
1515 */
1516 private function setBookedTimeSlots(&$freeSlots)
1517 {
1518 foreach (['available', 'occupied'] as $type) {
1519 foreach ($freeSlots[$type] as $dateString => $timeSlots) {
1520 foreach ($timeSlots as $timeString => $slots) {
1521 foreach ($slots as $slot) {
1522 if (isset($slot['d'])) {
1523 $appointmentStart = $this->intervalService->getSeconds($timeString . ':00') / 60;
1524
1525 $isSameDay = $appointmentStart + $slot['d'] <= 1440;
1526
1527 $appointmentEnd = $isSameDay
1528 ? $appointmentStart + $slot['d']
1529 : $appointmentStart + $slot['d'] - 1440;
1530
1531 if (isset($freeSlots['available'][$dateString])) {
1532 foreach ($freeSlots['available'][$dateString] as $inspectedTimeString => $inspectedSlotData) {
1533 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1534
1535 if (
1536 $inspectedSlot > $appointmentStart &&
1537 $inspectedSlot < ($isSameDay ? $appointmentEnd : 1440)
1538 ) {
1539 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1540 if ($slot['e'] === $inspectedSlot['e']) {
1541 $freeSlots['available'][$dateString][$inspectedTimeString][$inspectedIndex]['i'] = true;
1542
1543 break;
1544 }
1545 }
1546 }
1547 }
1548 }
1549
1550 if (!$isSameDay) {
1551 $nextDateString = (
1552 new \DateTime($dateString, DateTimeService::getTimeZone())
1553 )->modify('+1 day')->format('Y-m-d');
1554
1555 if (isset($freeSlots['available'][$nextDateString])) {
1556 foreach ($freeSlots['available'][$nextDateString] as $inspectedTimeString => $inspectedSlotData) {
1557 $inspectedSlot = $this->intervalService->getSeconds($inspectedTimeString . ':00') / 60;
1558
1559 if ($inspectedSlot < $appointmentEnd) {
1560 foreach ($inspectedSlotData as $inspectedIndex => $inspectedSlot) {
1561 if ($slot['e'] === $inspectedSlot['e']) {
1562 $freeSlots['available'][$nextDateString][$inspectedTimeString][$inspectedIndex] = true;
1563 }
1564 }
1565 }
1566 }
1567 }
1568 }
1569 }
1570 }
1571 }
1572 }
1573 }
1574 }
1575 }
1576