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