PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.31
Booking for Appointments and Events Calendar – Amelia v1.2.31
2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / src / Domain / Services / TimeSlot / TimeSlotService.php
ameliabooking / src / Domain / Services / TimeSlot Last commit date
TimeSlotService.php 1 year ago
TimeSlotService.php
1298 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
251 /** @var CustomerBooking $booking */
252 foreach ($app->getBookings()->getItems() as $booking) {
253 $persons += $booking->getPersons()->getValue();
254 }
255
256 $status = $app->getStatus()->getValue();
257
258 $appLocationId = $app->getLocationId() ? $app->getLocationId()->getValue() : null;
259
260 $hasCapacity =
261 $personsCount !== null &&
262 ($persons + $personsCount) <= $app->getService()->getMaxCapacity()->getValue() &&
263 !($app->isFull() ? $app->isFull()->getValue() : false);
264
265 $hasLocation =
266 !$locationId ||
267 ($app->getLocationId() && $app->getLocationId()->getValue() === $locationId) ||
268 (!$app->getLocationId() && $providerLocationId === $locationId) ||
269 ($appLocationId &&
270 $appLocationId === $locationId &&
271 $locations->getItem($appLocationId)->getStatus()->getValue() === Status::VISIBLE) ||
272 (!$appLocationId && $providerLocationId &&
273 $locations->getItem($providerLocationId)->getStatus()->getValue() === Status::VISIBLE);
274
275 $duration = $app->getBookingStart()->getValue()->diff($app->getBookingEnd()->getValue());
276
277 if (
278 ($hasLocation && $status === BookingStatus::APPROVED && $hasCapacity) ||
279 ($hasLocation && $status === BookingStatus::PENDING && ($bookIfPending || $hasCapacity))
280 ) {
281 $endDateTime = $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s');
282
283 $endDateTimeParts = explode(' ', $endDateTime);
284
285 $intervals[$occupiedDateStart]['available'][$app->getBookingStart()->getValue()->format('H:i')] =
286 [
287 'locationId' => $app->getLocationId() ?
288 $app->getLocationId()->getValue() : $providerLocationId,
289 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
290 'endDate' => $endDateTimeParts[0],
291 'endTime' => $endDateTimeParts[1],
292 'serviceId' => $serviceId,
293 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
294 ];
295 } else {
296 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
297 [
298 'locationId' => $app->getLocationId() ?
299 $app->getLocationId()->getValue() : $providerLocationId,
300 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
301 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
302 'serviceId' => $app->getServiceId()->getValue(),
303 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
304 ];
305 }
306 } elseif ($app->getServiceId()->getValue()) {
307 $duration = $app->getBookingStart()->getValue()->diff($app->getBookingEnd()->getValue());
308
309 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
310 [
311 'locationId' => $app->getLocationId() ?
312 $app->getLocationId()->getValue() : $providerLocationId,
313 'places' => 0,
314 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
315 'serviceId' => $app->getServiceId()->getValue(),
316 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
317 ];
318 }
319 }
320
321 return $intervals;
322 }
323
324 /**
325 * get provider day off dates.
326 *
327 * @param Provider $provider
328 *
329 * @return array
330 * @throws Exception
331 */
332 private function getProviderDayOffDates($provider)
333 {
334 $dates = [];
335
336 /** @var DayOff $dayOff */
337 foreach ($provider->getDayOffList()->getItems() as $dayOff) {
338 $endDateCopy = clone $dayOff->getEndDate()->getValue();
339
340 $dayOffPeriod = new DatePeriod(
341 $dayOff->getStartDate()->getValue(),
342 new DateInterval('P1D'),
343 $endDateCopy->modify('+1 day')
344 );
345
346 /** @var DateTime $date */
347 foreach ($dayOffPeriod as $date) {
348 $dateFormatted = $dayOff->getRepeat()->getValue() ?
349 $date->format('m-d') :
350 $date->format('Y-m-d');
351
352 $dates[$dateFormatted] = $dateFormatted;
353 }
354 }
355
356 return $dates;
357 }
358
359 /**
360 * get available appointment intervals.
361 *
362 * @param array $availableIntervals
363 * @param array $unavailableIntervals
364 *
365 * @return array
366 */
367 private function getAvailableIntervals(&$availableIntervals, $unavailableIntervals)
368 {
369 $parsedAvailablePeriod = [];
370
371 ksort($availableIntervals);
372 ksort($unavailableIntervals);
373
374 foreach ($availableIntervals as $available) {
375 $parsedAvailablePeriod[] = $available;
376
377 foreach ($unavailableIntervals as $unavailable) {
378 if ($parsedAvailablePeriod) {
379 $lastAvailablePeriod = $parsedAvailablePeriod[sizeof($parsedAvailablePeriod) - 1];
380
381 if ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
382 // unavailable interval is inside available interval
383 $fixedPeriod = array_pop($parsedAvailablePeriod);
384
385 if ($fixedPeriod[0] !== $unavailable[0]) {
386 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
387 }
388
389 if ($unavailable[1] !== $fixedPeriod[1]) {
390 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
391 }
392 } elseif (
393 $unavailable[0] <= $lastAvailablePeriod[0] &&
394 $unavailable[1] >= $lastAvailablePeriod[1]
395 ) {
396 // available interval is inside unavailable interval
397 array_pop($parsedAvailablePeriod);
398 } elseif (
399 $unavailable[0] <= $lastAvailablePeriod[0] &&
400 $unavailable[1] >= $lastAvailablePeriod[0] &&
401 $unavailable[1] <= $lastAvailablePeriod[1]
402 ) {
403 // unavailable interval intersect start of available interval
404 $fixedPeriod = array_pop($parsedAvailablePeriod);
405
406 if ($unavailable[1] !== $fixedPeriod[1]) {
407 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
408 }
409 } elseif (
410 $unavailable[0] >= $lastAvailablePeriod[0] &&
411 $unavailable[0] <= $lastAvailablePeriod[1] &&
412 $unavailable[1] >= $lastAvailablePeriod[1]
413 ) {
414 // unavailable interval intersect end of available interval
415 $fixedPeriod = array_pop($parsedAvailablePeriod);
416
417 if ($fixedPeriod[0] !== $unavailable[0]) {
418 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
419 }
420 }
421 }
422 }
423 }
424
425 return $parsedAvailablePeriod;
426 }
427
428 /**
429 * @param Service $service
430 * @param Provider $provider
431 * @param int $personsCount
432 *
433 * @return bool
434 *
435 * @throws Exception
436 */
437 private function getOnlyAppointmentsSlots($service, $provider, $personsCount)
438 {
439 $getOnlyAppointmentsSlots = false;
440
441 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
442 /** @var Service $providerService */
443 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
444
445 if ($personsCount < $providerService->getMinCapacity()->getValue()) {
446 $getOnlyAppointmentsSlots = true;
447 }
448 }
449
450 return $getOnlyAppointmentsSlots;
451 }
452
453 /** @noinspection MoreThanThreeArgumentsInspection */
454 /**
455 * @param Service $service
456 * @param int $locationId
457 * @param Collection $providers
458 * @param Collection $locations
459 * @param array $globalDaysOffDates
460 * @param DateTime $startDateTime
461 * @param DateTime $endDateTime
462 * @param int $personsCount
463 * @param boolean $bookIfPending
464 * @param boolean $bookIfNotMin
465 * @param boolean $bookAfterMin
466 * @param boolean $bookOverApp
467 * @param array $appointmentsCount
468 * @param boolean $allowAdminBookAtAnytime
469 *
470 * @return array
471 * @throws Exception
472 */
473 private function getFreeTime(
474 Service $service,
475 $locationId,
476 Collection $locations,
477 Collection $providers,
478 array $globalDaysOffDates,
479 DateTime $startDateTime,
480 DateTime $endDateTime,
481 $personsCount,
482 $bookIfPending,
483 $bookIfNotMin,
484 $bookAfterMin,
485 $bookOverApp,
486 $appointmentsCount,
487 $allowAdminBookAtAnytime
488 ) {
489
490 $weekDayIntervals = [];
491
492 $appointmentIntervals = [];
493
494 $daysOffDates = [];
495
496 $specialDayIntervals = [];
497
498 $getOnlyAppointmentsSlots = [];
499
500 $serviceId = $service->getId()->getValue();
501
502 /** @var Provider $provider */
503 foreach ($providers->getItems() as $provider) {
504 $providerId = $provider->getId()->getValue();
505
506 $getOnlyAppointmentsSlots[$providerId] = $bookIfNotMin && $bookAfterMin ? $this->getOnlyAppointmentsSlots(
507 $service,
508 $provider,
509 $personsCount
510 ) : false;
511
512 $daysOffDates[$providerId] = $this->getProviderDayOffDates($provider);
513
514 $weekDayIntervals[$providerId] = $this->scheduleService->getProviderWeekDaysIntervals(
515 $provider,
516 $locations,
517 $locationId,
518 $serviceId
519 );
520
521 $specialDayIntervals[$providerId] = $this->scheduleService->getProviderSpecialDayIntervals(
522 $provider,
523 $locations,
524 $locationId,
525 $serviceId
526 );
527
528 $appointmentIntervals[$providerId] = $this->getProviderAppointmentIntervals(
529 $provider,
530 $locations,
531 $serviceId,
532 $locationId,
533 $personsCount,
534 $bookIfPending,
535 $bookOverApp,
536 $weekDayIntervals[$providerId],
537 $specialDayIntervals[$providerId]
538 );
539 }
540
541 $freeDateIntervals = [];
542
543 foreach ($appointmentIntervals as $providerKey => $providerDates) {
544 foreach ((array)$providerDates as $dateKey => $dateIntervals) {
545 $dayIndex = DateTimeService::getDayIndex($dateKey);
546
547 $specialDayDateKey = null;
548
549 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
550 if (array_key_exists($dateKey, $specialDays['dates'])) {
551 $specialDayDateKey = $specialDayKey;
552 break;
553 }
554 }
555
556 if (
557 $specialDayDateKey !== null &&
558 isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])
559 ) {
560 // get free intervals if it is special day
561 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
562 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
563 !empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []
564 );
565 } elseif (
566 isset($weekDayIntervals[$providerKey][$dayIndex]['free']) &&
567 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
568 ) {
569 // get free intervals if it is working day
570 $unavailableIntervals =
571 $weekDayIntervals[$providerKey][$dayIndex]['busy'] + (!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []);
572
573 $intersectedTimes = array_intersect(
574 array_keys($weekDayIntervals[$providerKey][$dayIndex]['busy']),
575 array_keys(!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : [])
576 );
577
578 foreach ($intersectedTimes as $time) {
579 $unavailableIntervals[$time] =
580 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] >
581 $dateIntervals['occupied'][$time] ?
582 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] :
583 $dateIntervals['occupied'][$time];
584 }
585
586 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
587 $weekDayIntervals[$providerKey][$dayIndex]['free'],
588 $unavailableIntervals ?: []
589 );
590 }
591 }
592 }
593
594 $startDateTime = clone $startDateTime;
595
596 $startDateTime->setTime(0, 0);
597
598 $endDateTime = clone $endDateTime;
599
600 $endDateTime->modify('+1 day')->setTime(0, 0);
601
602 // create calendar
603 $period = new DatePeriod(
604 $startDateTime,
605 new DateInterval('P1D'),
606 $endDateTime
607 );
608
609 $calendar = [];
610
611 /** @var DateTime $day */
612 foreach ($period as $day) {
613 $currentDate = $day->format('Y-m-d');
614 $dayIndex = (int)$day->format('N');
615
616 $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) ||
617 array_key_exists($day->format('m-d'), $globalDaysOffDates);
618
619 if (!$isGlobalDayOff) {
620 foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) {
621 $isProviderDayOff = array_key_exists($currentDate, $daysOffDates[$providerKey]) ||
622 array_key_exists($day->format('m-d'), $daysOffDates[$providerKey]);
623
624 $specialDayDateKey = null;
625
626 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
627 if (array_key_exists($currentDate, $specialDays['dates'])) {
628 $specialDayDateKey = $specialDayKey;
629 break;
630 }
631 }
632
633 if (!$isProviderDayOff) {
634 // daily limit per employee
635 if (
636 !$allowAdminBookAtAnytime &&
637 !empty($appointmentsCount['limitCount']) &&
638 !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
639 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']
640 ) {
641 continue;
642 }
643
644 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
645 // get date intervals if there are appointments (special or working day)
646 $calendar[$currentDate][$providerKey] = [
647 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
648 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
649 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
650 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
651 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
652 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
653 ];
654 } else {
655 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
656 // get date intervals if it is special day with out appointments
657 $calendar[$currentDate][$providerKey] = [
658 'slots' => [],
659 'full' => [],
660 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
661 [] :
662 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
663 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
664 $appointmentsCount[$providerKey][$currentDate] :
665 0
666 ];
667 } elseif (
668 isset($weekDayIntervals[$providerKey][$dayIndex]) &&
669 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
670 ) {
671 // get date intervals if it is working day without appointments
672 $calendar[$currentDate][$providerKey] = [
673 'slots' => [],
674 'full' => [],
675 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
676 [] :
677 $weekDayIntervals[$providerKey][$dayIndex]['free'],
678 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
679 $appointmentsCount[$providerKey][$currentDate] :
680 0
681 ];
682 }
683 }
684 }
685 }
686 }
687 }
688
689 return $calendar;
690 }
691
692 /** @noinspection MoreThanThreeArgumentsInspection */
693 /**
694 * @param Service $service
695 * @param int $requiredTime
696 * @param array $freeIntervals
697 * @param array $resourcedIntervals
698 * @param int $slotLength
699 * @param DateTime $startDateTime
700 * @param bool $serviceDurationAsSlot
701 * @param bool $bufferTimeInSlot
702 * @param bool $isFrontEndBooking
703 * @param String $timeZone
704 *
705 * @return array
706 */
707 private function getAppointmentFreeSlots(
708 $service,
709 $requiredTime,
710 &$freeIntervals,
711 $resourcedIntervals,
712 $slotLength,
713 $startDateTime,
714 $serviceDurationAsSlot,
715 $bufferTimeInSlot,
716 $isFrontEndBooking,
717 $timeZone
718 ) {
719 $availableResult = [];
720
721 $occupiedResult = [];
722
723 $realRequiredTime = $requiredTime -
724 $service->getTimeBefore()->getValue() -
725 $service->getTimeAfter()->getValue();
726
727 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
728 $requiredTime = $requiredTime -
729 $service->getTimeBefore()->getValue() -
730 $service->getTimeAfter()->getValue();
731 }
732
733 $currentDateTime = DateTimeService::getNowDateTimeObject();
734
735 $currentDateString = $currentDateTime->format('Y-m-d');
736
737 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
738
739 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
740
741 $currentDateFormatted = $currentDateTime->format('Y-m-d');
742
743 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
744
745 $startDateFormatted = $startDateTime->format('Y-m-d');
746
747 $bookingLength = $serviceDurationAsSlot && $isFrontEndBooking ? $requiredTime : $slotLength;
748
749 $appCount = [];
750
751 $isContinuousTime = false;
752
753 $continuousTimeSlot = null;
754
755 foreach ($freeIntervals as $dateKey => $dateProviders) {
756 foreach ((array)$dateProviders as $providerKey => $provider) {
757 foreach ((array)$provider['intervals'] as $timePeriod) {
758 $moveStart = false;
759
760 if ($timePeriod[0] === 0 && $isContinuousTime && $continuousTimeSlot !== null) {
761 $isContinuousTime = false;
762
763 $moveStart = true;
764 }
765
766 if ($timePeriod[1] === 86400) {
767 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
768 $dateKey . ' 00:00:00',
769 $timeZone
770 )->modify('+1 days')->format('Y-m-d');
771
772 if (
773 isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
774 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
775 ) {
776 $isContinuousTime = true;
777
778 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
779
780 $timePeriod[1] += (
781 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
782 ? $realRequiredTime + $service->getTimeAfter()->getValue()
783 : $nextDayInterval);
784 }
785 }
786
787 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
788 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
789 }
790
791 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
792
793 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
794
795 $numberOfSlots = (int)(
796 floor(
797 (
798 $timePeriod[1] -
799 $providerTimeStart -
800 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
801 ) / $bookingLength
802 ) + 1
803 );
804
805 $inspectResourceIndexes = [];
806
807 if (isset($resourcedIntervals[$dateKey])) {
808 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
809 if (
810 array_intersect(
811 $timePeriod[2],
812 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
813 )
814 ) {
815 $inspectResourceIndexes[] = $resourceIndex;
816 }
817 }
818 }
819
820 $providerPeriodSlots = [];
821
822 $achievedLength = 0;
823
824 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
825 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
826
827 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
828
829 $numberOfSlots = (int)(floor(($timePeriod[1] - $providerTimeStart - $requiredTime) / $bookingLength) + 1);
830 }
831
832 if ($moveStart) {
833 $continuousTimeSlot = null;
834 }
835
836 for ($i = 0; $i < $numberOfSlots; $i++) {
837 $achievedLength += $bookingLength;
838
839 $timeSlot = $customerTimeStart + $i * $bookingLength;
840
841 if (
842 (
843 $startDateFormatted !== $dateKey &&
844 ($serviceDurationAsSlot &&
845 !$bufferTimeInSlot ? $timeSlot <= $timePeriod[1] - $requiredTime : true)
846 ) ||
847 ($startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot) ||
848 ($startDateFormatted ===
849 $currentDateFormatted &&
850 $startDateFormatted === $dateKey &&
851 $startTimeInSeconds < $timeSlot &&
852 $currentTimeInSeconds < $timeSlot)
853 ) {
854 $timeSlotEnd = $timeSlot + $bookingLength;
855
856 $filteredLocationsIds = $timePeriod[2];
857
858 foreach ($inspectResourceIndexes as $resourceIndex) {
859 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
860 if (
861 ($start >= $timeSlot && $start < $timeSlotEnd) ||
862 ($end > $timeSlot && $end <= $timeSlotEnd) ||
863 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
864 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
865 ) {
866 $filteredLocationsIds = array_diff(
867 $filteredLocationsIds,
868 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
869 );
870
871 if (!$filteredLocationsIds) {
872 if ($achievedLength < $requiredTime) {
873 $providerPeriodSlots = [];
874
875 $achievedLength = 0;
876 }
877
878 continue 3;
879 }
880
881 $removedLocationsIds = array_diff(
882 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
883 $filteredLocationsIds
884 );
885
886 if ($removedLocationsIds && $achievedLength < $requiredTime) {
887 $parsedPeriodSlots = [];
888
889 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
890 if (
891 $start >= $previousTimeSlot &&
892 $start < $previousTimeSlot + $requiredTime
893 ) {
894 foreach ($periodSlotData as $data) {
895 if (!in_array($data[1], $removedLocationsIds)) {
896 $parsedPeriodSlots[$previousTimeSlot][] = $data;
897 }
898 }
899 } else {
900 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
901 }
902 }
903
904 $providerPeriodSlots = $parsedPeriodSlots;
905 }
906 }
907 }
908 }
909
910 if (!$timePeriod[2]) {
911 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
912 } elseif ($filteredLocationsIds) {
913 foreach ($filteredLocationsIds as $locationId) {
914 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
915 }
916 }
917 }
918 }
919
920 foreach ($providerPeriodSlots as $timeSlot => $data) {
921 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
922 . sprintf('%02d', floor(($timeSlot / 60) % 60));
923
924 if ($time !== '24:00') {
925 $availableResult[$dateKey][$time] = $data;
926
927 if ($isContinuousTime) {
928 $continuousTimeSlot = $timeSlot;
929 }
930 }
931 }
932 }
933
934 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
935 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
936
937 if (
938 $currentDateString === $dateKey &&
939 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
940 ) {
941 continue;
942 }
943
944 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
945
946 $newEndInSeconds = $startInSeconds + $realRequiredTime;
947
948 if (
949 $newEndInSeconds !== 86400 &&
950 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
951 ) {
952 if ($dateKey !== $appointmentData['endDate']) {
953 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
954 $dateKey . ' 00:00:00',
955 $timeZone
956 )->modify('+1 days')->format('Y-m-d');
957
958 if (
959 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
960 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
961 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
962 ) {
963 continue;
964 }
965 } elseif ($newEndInSeconds > 86400) {
966 $nextIntervalIsValid = false;
967
968 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
969 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
970 $nextIntervalIsValid = true;
971
972 break;
973 }
974 }
975
976 if (!$nextIntervalIsValid) {
977 continue;
978 }
979
980 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
981 $dateKey . ' 00:00:00',
982 $timeZone
983 )->modify('+1 days')->format('Y-m-d');
984
985 if (
986 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
987 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
988 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
989 ) {
990 continue;
991 }
992 } else {
993 $nextIntervalIsValid = false;
994
995 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
996 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
997 $nextIntervalIsValid = true;
998
999 break;
1000 }
1001 }
1002
1003 if (!$nextIntervalIsValid) {
1004 continue;
1005 }
1006 }
1007 }
1008
1009 $availableResult[$dateKey][$appointmentTime] = [
1010 [
1011 $providerKey,
1012 $appointmentData['locationId'],
1013 $appointmentData['places'],
1014 $appointmentData['serviceId'],
1015 $appointmentData['duration'],
1016 ]
1017 ];
1018 }
1019
1020 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
1021 $occupiedResult[$dateKey][$appointmentTime][] = [
1022 $providerKey,
1023 $appointmentData['locationId'],
1024 $appointmentData['places'],
1025 $appointmentData['serviceId'],
1026 $appointmentData['duration'],
1027 ];
1028 }
1029
1030 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
1031 }
1032 }
1033
1034 return [
1035 'available' => $availableResult,
1036 'occupied' => $occupiedResult,
1037 'appCount' => $appCount
1038 ];
1039 }
1040
1041 /**
1042 * @param array $slots
1043 * @param string $timeZone
1044 *
1045 * @return array
1046 * @throws Exception
1047 */
1048 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
1049 {
1050 $convertedProviderSlots = [];
1051
1052 foreach ($slots as $slotDate => $slotTimes) {
1053 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
1054 $convertedSlotParts = explode(
1055 ' ',
1056 DateTimeService::getDateTimeObjectInTimeZone(
1057 $slotDate . ' ' . $slotTime,
1058 $timeZone
1059 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
1060 );
1061
1062 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1063 }
1064 }
1065
1066 return $convertedProviderSlots;
1067 }
1068
1069
1070 /**
1071 * @param Collection $appointments
1072 * @param int $excludeAppointmentId
1073 *
1074 * @return array
1075 * @throws Exception
1076 */
1077 public function getAppointmentCount($appointments, $excludeAppointmentId)
1078 {
1079 $appCount = [];
1080
1081 /** @var Appointment $appointment */
1082 foreach ($appointments->getItems() as $appointment) {
1083 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1084 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1085 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1086 } else {
1087 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1088 }
1089 }
1090 }
1091
1092 return $appCount;
1093 }
1094
1095 /** @noinspection MoreThanThreeArgumentsInspection */
1096 /**
1097 * @param array $settings
1098 * @param array $props
1099 * @param SlotsEntities $slotsEntities
1100 * @param Collection $appointments
1101 *
1102 * @return array
1103 * @throws Exception
1104 */
1105 public function getSlots($settings, $props, $slotsEntities, $appointments)
1106 {
1107 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1108
1109 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1110 $this->resourceService->manageResources(
1111 $slotsEntities->getResources(),
1112 $appointments,
1113 $slotsEntities->getLocations(),
1114 $slotsEntities->getServices()->getItem($props['serviceId']),
1115 $slotsEntities->getProviders(),
1116 $props['locationId'],
1117 $props['excludeAppointmentId'],
1118 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1119 ) : [];
1120
1121 $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1122
1123 $this->providerService->addAppointmentsToAppointmentList(
1124 $slotsEntities->getProviders(),
1125 $appointments,
1126 $settings['isGloballyBusySlot']
1127 );
1128
1129 return $this->getCalculatedFreeSlots(
1130 $settings,
1131 $props,
1132 $slotsEntities,
1133 $resourcedLocationsIntervals,
1134 $appointmentsCount
1135 );
1136 }
1137
1138 /** @noinspection MoreThanThreeArgumentsInspection */
1139 /**
1140 * @param array $settings
1141 * @param array $props
1142 * @param SlotsEntities $slotsEntities
1143 * @param array $resourcedLocationsIntervals
1144 * @param array $appointmentsCount
1145 *
1146 * @return array
1147 * @throws Exception
1148 */
1149 private function getCalculatedFreeSlots(
1150 $settings,
1151 $props,
1152 $slotsEntities,
1153 $resourcedLocationsIntervals,
1154 $appointmentsCount
1155 ) {
1156 $freeProvidersSlots = [];
1157
1158 /** @var DateTime $startDateTime */
1159 $startDateTime = $props['startDateTime'];
1160
1161 /** @var DateTime $endDateTime */
1162 $endDateTime = $props['endDateTime'];
1163
1164 /** @var Service $service */
1165 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1166
1167 /** @var Collection $providers */
1168 $providers = $slotsEntities->getProviders();
1169
1170 /** @var Collection $locations */
1171 $locations = $slotsEntities->getLocations();
1172
1173 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1174 $service,
1175 $props['extras']
1176 );
1177
1178 /** @var Provider $provider */
1179 foreach ($providers->getItems() as $provider) {
1180 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1181 /** @var Service $providerService */
1182 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1183
1184 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1185 continue;
1186 }
1187 }
1188
1189 $providerContainer = new Collection();
1190
1191 if ($provider->getTimeZone()) {
1192 $this->providerService->modifyProviderTimeZone(
1193 $provider,
1194 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1195 $startDateTime,
1196 $endDateTime
1197 );
1198 }
1199
1200 $start = $provider->getTimeZone() ?
1201 DateTimeService::getCustomDateTimeObjectInTimeZone(
1202 $startDateTime->format('Y-m-d H:i'),
1203 $provider->getTimeZone()->getValue()
1204 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1205
1206 $end = $provider->getTimeZone() ?
1207 DateTimeService::getCustomDateTimeObjectInTimeZone(
1208 $endDateTime->format('Y-m-d H:i'),
1209 $provider->getTimeZone()->getValue()
1210 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1211
1212 $providerContainer->addItem($provider, $provider->getId()->getValue());
1213
1214 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1215 $settings['limitPerEmployee']['numberOfApp'] : null;
1216
1217 $freeIntervals = $this->getFreeTime(
1218 $service,
1219 $props['locationId'],
1220 $locations,
1221 $providerContainer,
1222 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1223 [] : $settings['globalDaysOff'],
1224 $start,
1225 $end,
1226 $props['personsCount'],
1227 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1228 $settings['allowBookingIfNotMin'],
1229 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1230 !empty($settings['allowAdminBookOverApp']),
1231 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount],
1232 !empty($settings['allowAdminBookAtAnyTime']),
1233 );
1234
1235 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1236 $service,
1237 $requiredTime,
1238 $freeIntervals,
1239 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1240 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1241 $settings['timeSlotLength'] ?: $requiredTime,
1242 $start,
1243 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1244 $settings['serviceDurationAsSlot'],
1245 $settings['bufferTimeInSlot'],
1246 true,
1247 $provider->getTimeZone() ?
1248 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName()
1249 );
1250 }
1251
1252 $freeSlots = [
1253 'available' => [],
1254 'occupied' => [],
1255 'appCount' => [],
1256 'duration' => $requiredTime / 60,
1257 ];
1258
1259 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1260 /** @var Provider $provider */
1261 $provider = $providers->getItem($providerKey);
1262
1263 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1264
1265 foreach (['available', 'occupied'] as $type) {
1266 if ($provider->getTimeZone()) {
1267 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1268 $providerSlots[$type],
1269 $provider->getTimeZone()->getValue()
1270 );
1271 }
1272
1273 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1274 foreach ($dateSlots as $timeKey => $slotData) {
1275 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1276 $freeSlots[$type][$dateKey][$timeKey] = [];
1277 }
1278
1279 foreach ($slotData as $item) {
1280 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1281 }
1282
1283 if (isset($freeSlots[$type][$dateKey])) {
1284 if (!$freeSlots[$type][$dateKey]) {
1285 unset($freeSlots[$type][$dateKey]);
1286 } else {
1287 ksort($freeSlots[$type][$dateKey]);
1288 }
1289 }
1290 }
1291 }
1292 }
1293 }
1294
1295 return $freeSlots;
1296 }
1297 }
1298