PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.27
Booking for Appointments and Events Calendar – Amelia v1.2.27
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
1293 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 *
469 * @return array
470 * @throws Exception
471 */
472 private function getFreeTime(
473 Service $service,
474 $locationId,
475 Collection $locations,
476 Collection $providers,
477 array $globalDaysOffDates,
478 DateTime $startDateTime,
479 DateTime $endDateTime,
480 $personsCount,
481 $bookIfPending,
482 $bookIfNotMin,
483 $bookAfterMin,
484 $bookOverApp,
485 $appointmentsCount
486 ) {
487
488 $weekDayIntervals = [];
489
490 $appointmentIntervals = [];
491
492 $daysOffDates = [];
493
494 $specialDayIntervals = [];
495
496 $getOnlyAppointmentsSlots = [];
497
498 $serviceId = $service->getId()->getValue();
499
500 /** @var Provider $provider */
501 foreach ($providers->getItems() as $provider) {
502 $providerId = $provider->getId()->getValue();
503
504 $getOnlyAppointmentsSlots[$providerId] = $bookIfNotMin && $bookAfterMin ? $this->getOnlyAppointmentsSlots(
505 $service,
506 $provider,
507 $personsCount
508 ) : false;
509
510 $daysOffDates[$providerId] = $this->getProviderDayOffDates($provider);
511
512 $weekDayIntervals[$providerId] = $this->scheduleService->getProviderWeekDaysIntervals(
513 $provider,
514 $locations,
515 $locationId,
516 $serviceId
517 );
518
519 $specialDayIntervals[$providerId] = $this->scheduleService->getProviderSpecialDayIntervals(
520 $provider,
521 $locations,
522 $locationId,
523 $serviceId
524 );
525
526 $appointmentIntervals[$providerId] = $this->getProviderAppointmentIntervals(
527 $provider,
528 $locations,
529 $serviceId,
530 $locationId,
531 $personsCount,
532 $bookIfPending,
533 $bookOverApp,
534 $weekDayIntervals[$providerId],
535 $specialDayIntervals[$providerId]
536 );
537 }
538
539 $freeDateIntervals = [];
540
541 foreach ($appointmentIntervals as $providerKey => $providerDates) {
542 foreach ((array)$providerDates as $dateKey => $dateIntervals) {
543 $dayIndex = DateTimeService::getDayIndex($dateKey);
544
545 $specialDayDateKey = null;
546
547 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
548 if (array_key_exists($dateKey, $specialDays['dates'])) {
549 $specialDayDateKey = $specialDayKey;
550 break;
551 }
552 }
553
554 if (
555 $specialDayDateKey !== null &&
556 isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])
557 ) {
558 // get free intervals if it is special day
559 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
560 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
561 !empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []
562 );
563 } elseif (
564 isset($weekDayIntervals[$providerKey][$dayIndex]['free']) &&
565 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
566 ) {
567 // get free intervals if it is working day
568 $unavailableIntervals =
569 $weekDayIntervals[$providerKey][$dayIndex]['busy'] + (!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []);
570
571 $intersectedTimes = array_intersect(
572 array_keys($weekDayIntervals[$providerKey][$dayIndex]['busy']),
573 array_keys(!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : [])
574 );
575
576 foreach ($intersectedTimes as $time) {
577 $unavailableIntervals[$time] =
578 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] >
579 $dateIntervals['occupied'][$time] ?
580 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] :
581 $dateIntervals['occupied'][$time];
582 }
583
584 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
585 $weekDayIntervals[$providerKey][$dayIndex]['free'],
586 $unavailableIntervals ?: []
587 );
588 }
589 }
590 }
591
592 $startDateTime = clone $startDateTime;
593
594 $startDateTime->setTime(0, 0);
595
596 $endDateTime = clone $endDateTime;
597
598 $endDateTime->modify('+1 day')->setTime(0, 0);
599
600 // create calendar
601 $period = new DatePeriod(
602 $startDateTime,
603 new DateInterval('P1D'),
604 $endDateTime
605 );
606
607 $calendar = [];
608
609 /** @var DateTime $day */
610 foreach ($period as $day) {
611 $currentDate = $day->format('Y-m-d');
612 $dayIndex = (int)$day->format('N');
613
614 $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) ||
615 array_key_exists($day->format('m-d'), $globalDaysOffDates);
616
617 if (!$isGlobalDayOff) {
618 foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) {
619 $isProviderDayOff = array_key_exists($currentDate, $daysOffDates[$providerKey]) ||
620 array_key_exists($day->format('m-d'), $daysOffDates[$providerKey]);
621
622 $specialDayDateKey = null;
623
624 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
625 if (array_key_exists($currentDate, $specialDays['dates'])) {
626 $specialDayDateKey = $specialDayKey;
627 break;
628 }
629 }
630
631 if (!$isProviderDayOff) {
632 if (
633 !empty($appointmentsCount['limitCount']) &&
634 !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
635 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']
636 ) {
637 continue;
638 }
639
640 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
641 // get date intervals if there are appointments (special or working day)
642 $calendar[$currentDate][$providerKey] = [
643 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
644 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
645 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
646 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
647 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
648 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
649 ];
650 } else {
651 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
652 // get date intervals if it is special day with out appointments
653 $calendar[$currentDate][$providerKey] = [
654 'slots' => [],
655 'full' => [],
656 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
657 [] :
658 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
659 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
660 $appointmentsCount[$providerKey][$currentDate] :
661 0
662 ];
663 } elseif (
664 isset($weekDayIntervals[$providerKey][$dayIndex]) &&
665 !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])
666 ) {
667 // get date intervals if it is working day without appointments
668 $calendar[$currentDate][$providerKey] = [
669 'slots' => [],
670 'full' => [],
671 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ?
672 [] :
673 $weekDayIntervals[$providerKey][$dayIndex]['free'],
674 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ?
675 $appointmentsCount[$providerKey][$currentDate] :
676 0
677 ];
678 }
679 }
680 }
681 }
682 }
683 }
684
685 return $calendar;
686 }
687
688 /** @noinspection MoreThanThreeArgumentsInspection */
689 /**
690 * @param Service $service
691 * @param int $requiredTime
692 * @param array $freeIntervals
693 * @param array $resourcedIntervals
694 * @param int $slotLength
695 * @param DateTime $startDateTime
696 * @param bool $serviceDurationAsSlot
697 * @param bool $bufferTimeInSlot
698 * @param bool $isFrontEndBooking
699 * @param String $timeZone
700 *
701 * @return array
702 */
703 private function getAppointmentFreeSlots(
704 $service,
705 $requiredTime,
706 &$freeIntervals,
707 $resourcedIntervals,
708 $slotLength,
709 $startDateTime,
710 $serviceDurationAsSlot,
711 $bufferTimeInSlot,
712 $isFrontEndBooking,
713 $timeZone
714 ) {
715 $availableResult = [];
716
717 $occupiedResult = [];
718
719 $realRequiredTime = $requiredTime -
720 $service->getTimeBefore()->getValue() -
721 $service->getTimeAfter()->getValue();
722
723 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
724 $requiredTime = $requiredTime -
725 $service->getTimeBefore()->getValue() -
726 $service->getTimeAfter()->getValue();
727 }
728
729 $currentDateTime = DateTimeService::getNowDateTimeObject();
730
731 $currentDateString = $currentDateTime->format('Y-m-d');
732
733 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
734
735 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
736
737 $currentDateFormatted = $currentDateTime->format('Y-m-d');
738
739 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
740
741 $startDateFormatted = $startDateTime->format('Y-m-d');
742
743 $bookingLength = $serviceDurationAsSlot && $isFrontEndBooking ? $requiredTime : $slotLength;
744
745 $appCount = [];
746
747 $isContinuousTime = false;
748
749 $continuousTimeSlot = null;
750
751 foreach ($freeIntervals as $dateKey => $dateProviders) {
752 foreach ((array)$dateProviders as $providerKey => $provider) {
753 foreach ((array)$provider['intervals'] as $timePeriod) {
754 $moveStart = false;
755
756 if ($timePeriod[0] === 0 && $isContinuousTime && $continuousTimeSlot !== null) {
757 $isContinuousTime = false;
758
759 $moveStart = true;
760 }
761
762 if ($timePeriod[1] === 86400) {
763 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
764 $dateKey . ' 00:00:00',
765 $timeZone
766 )->modify('+1 days')->format('Y-m-d');
767
768 if (
769 isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
770 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
771 ) {
772 $isContinuousTime = true;
773
774 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
775
776 $timePeriod[1] += (
777 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
778 ? $realRequiredTime + $service->getTimeAfter()->getValue()
779 : $nextDayInterval);
780 }
781 }
782
783 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
784 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
785 }
786
787 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
788
789 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
790
791 $numberOfSlots = (int)(
792 floor(
793 (
794 $timePeriod[1] -
795 $providerTimeStart -
796 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
797 ) / $bookingLength
798 ) + 1
799 );
800
801 $inspectResourceIndexes = [];
802
803 if (isset($resourcedIntervals[$dateKey])) {
804 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
805 if (
806 array_intersect(
807 $timePeriod[2],
808 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
809 )
810 ) {
811 $inspectResourceIndexes[] = $resourceIndex;
812 }
813 }
814 }
815
816 $providerPeriodSlots = [];
817
818 $achievedLength = 0;
819
820 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
821 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
822
823 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
824
825 $numberOfSlots = (int)(floor(($timePeriod[1] - $providerTimeStart - $requiredTime) / $bookingLength) + 1);
826 }
827
828 if ($moveStart) {
829 $continuousTimeSlot = null;
830 }
831
832 for ($i = 0; $i < $numberOfSlots; $i++) {
833 $achievedLength += $bookingLength;
834
835 $timeSlot = $customerTimeStart + $i * $bookingLength;
836
837 if (
838 (
839 $startDateFormatted !== $dateKey &&
840 ($serviceDurationAsSlot &&
841 !$bufferTimeInSlot ? $timeSlot <= $timePeriod[1] - $requiredTime : true)
842 ) ||
843 ($startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot) ||
844 ($startDateFormatted ===
845 $currentDateFormatted &&
846 $startDateFormatted === $dateKey &&
847 $startTimeInSeconds < $timeSlot &&
848 $currentTimeInSeconds < $timeSlot)
849 ) {
850 $timeSlotEnd = $timeSlot + $bookingLength;
851
852 $filteredLocationsIds = $timePeriod[2];
853
854 foreach ($inspectResourceIndexes as $resourceIndex) {
855 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
856 if (
857 ($start >= $timeSlot && $start < $timeSlotEnd) ||
858 ($end > $timeSlot && $end <= $timeSlotEnd) ||
859 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
860 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
861 ) {
862 $filteredLocationsIds = array_diff(
863 $filteredLocationsIds,
864 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
865 );
866
867 if (!$filteredLocationsIds) {
868 if ($achievedLength < $requiredTime) {
869 $providerPeriodSlots = [];
870
871 $achievedLength = 0;
872 }
873
874 continue 3;
875 }
876
877 $removedLocationsIds = array_diff(
878 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
879 $filteredLocationsIds
880 );
881
882 if ($removedLocationsIds && $achievedLength < $requiredTime) {
883 $parsedPeriodSlots = [];
884
885 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
886 if (
887 $start >= $previousTimeSlot &&
888 $start < $previousTimeSlot + $requiredTime
889 ) {
890 foreach ($periodSlotData as $data) {
891 if (!in_array($data[1], $removedLocationsIds)) {
892 $parsedPeriodSlots[$previousTimeSlot][] = $data;
893 }
894 }
895 } else {
896 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
897 }
898 }
899
900 $providerPeriodSlots = $parsedPeriodSlots;
901 }
902 }
903 }
904 }
905
906 if (!$timePeriod[2]) {
907 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
908 } elseif ($filteredLocationsIds) {
909 foreach ($filteredLocationsIds as $locationId) {
910 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
911 }
912 }
913 }
914 }
915
916 foreach ($providerPeriodSlots as $timeSlot => $data) {
917 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
918 . sprintf('%02d', floor(($timeSlot / 60) % 60));
919
920 if ($time !== '24:00') {
921 $availableResult[$dateKey][$time] = $data;
922
923 if ($isContinuousTime) {
924 $continuousTimeSlot = $timeSlot;
925 }
926 }
927 }
928 }
929
930 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
931 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
932
933 if (
934 $currentDateString === $dateKey &&
935 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
936 ) {
937 continue;
938 }
939
940 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
941
942 $newEndInSeconds = $startInSeconds + $realRequiredTime;
943
944 if (
945 $newEndInSeconds !== 86400 &&
946 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
947 ) {
948 if ($dateKey !== $appointmentData['endDate']) {
949 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
950 $dateKey . ' 00:00:00',
951 $timeZone
952 )->modify('+1 days')->format('Y-m-d');
953
954 if (
955 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
956 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
957 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
958 ) {
959 continue;
960 }
961 } elseif ($newEndInSeconds > 86400) {
962 $nextIntervalIsValid = false;
963
964 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
965 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
966 $nextIntervalIsValid = true;
967
968 break;
969 }
970 }
971
972 if (!$nextIntervalIsValid) {
973 continue;
974 }
975
976 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
977 $dateKey . ' 00:00:00',
978 $timeZone
979 )->modify('+1 days')->format('Y-m-d');
980
981 if (
982 !isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
983 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
984 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
985 ) {
986 continue;
987 }
988 } else {
989 $nextIntervalIsValid = false;
990
991 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
992 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
993 $nextIntervalIsValid = true;
994
995 break;
996 }
997 }
998
999 if (!$nextIntervalIsValid) {
1000 continue;
1001 }
1002 }
1003 }
1004
1005 $availableResult[$dateKey][$appointmentTime] = [
1006 [
1007 $providerKey,
1008 $appointmentData['locationId'],
1009 $appointmentData['places'],
1010 $appointmentData['serviceId'],
1011 $appointmentData['duration'],
1012 ]
1013 ];
1014 }
1015
1016 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
1017 $occupiedResult[$dateKey][$appointmentTime][] = [
1018 $providerKey,
1019 $appointmentData['locationId'],
1020 $appointmentData['places'],
1021 $appointmentData['serviceId'],
1022 $appointmentData['duration'],
1023 ];
1024 }
1025
1026 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
1027 }
1028 }
1029
1030 return [
1031 'available' => $availableResult,
1032 'occupied' => $occupiedResult,
1033 'appCount' => $appCount
1034 ];
1035 }
1036
1037 /**
1038 * @param array $slots
1039 * @param string $timeZone
1040 *
1041 * @return array
1042 * @throws Exception
1043 */
1044 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
1045 {
1046 $convertedProviderSlots = [];
1047
1048 foreach ($slots as $slotDate => $slotTimes) {
1049 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
1050 $convertedSlotParts = explode(
1051 ' ',
1052 DateTimeService::getDateTimeObjectInTimeZone(
1053 $slotDate . ' ' . $slotTime,
1054 $timeZone
1055 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
1056 );
1057
1058 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1059 }
1060 }
1061
1062 return $convertedProviderSlots;
1063 }
1064
1065
1066 /**
1067 * @param Collection $appointments
1068 * @param int $excludeAppointmentId
1069 *
1070 * @return array
1071 * @throws Exception
1072 */
1073 public function getAppointmentCount($appointments, $excludeAppointmentId)
1074 {
1075 $appCount = [];
1076
1077 /** @var Appointment $appointment */
1078 foreach ($appointments->getItems() as $appointment) {
1079 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1080 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1081 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1082 } else {
1083 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1084 }
1085 }
1086 }
1087
1088 return $appCount;
1089 }
1090
1091 /** @noinspection MoreThanThreeArgumentsInspection */
1092 /**
1093 * @param array $settings
1094 * @param array $props
1095 * @param SlotsEntities $slotsEntities
1096 * @param Collection $appointments
1097 *
1098 * @return array
1099 * @throws Exception
1100 */
1101 public function getSlots($settings, $props, $slotsEntities, $appointments)
1102 {
1103 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1104
1105 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1106 $this->resourceService->manageResources(
1107 $slotsEntities->getResources(),
1108 $appointments,
1109 $slotsEntities->getLocations(),
1110 $slotsEntities->getServices()->getItem($props['serviceId']),
1111 $slotsEntities->getProviders(),
1112 $props['locationId'],
1113 $props['excludeAppointmentId'],
1114 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1115 ) : [];
1116
1117 $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1118
1119 $this->providerService->addAppointmentsToAppointmentList(
1120 $slotsEntities->getProviders(),
1121 $appointments,
1122 $settings['isGloballyBusySlot']
1123 );
1124
1125 return $this->getCalculatedFreeSlots(
1126 $settings,
1127 $props,
1128 $slotsEntities,
1129 $resourcedLocationsIntervals,
1130 $appointmentsCount
1131 );
1132 }
1133
1134 /** @noinspection MoreThanThreeArgumentsInspection */
1135 /**
1136 * @param array $settings
1137 * @param array $props
1138 * @param SlotsEntities $slotsEntities
1139 * @param array $resourcedLocationsIntervals
1140 * @param array $appointmentsCount
1141 *
1142 * @return array
1143 * @throws Exception
1144 */
1145 private function getCalculatedFreeSlots(
1146 $settings,
1147 $props,
1148 $slotsEntities,
1149 $resourcedLocationsIntervals,
1150 $appointmentsCount
1151 ) {
1152 $freeProvidersSlots = [];
1153
1154 /** @var DateTime $startDateTime */
1155 $startDateTime = $props['startDateTime'];
1156
1157 /** @var DateTime $endDateTime */
1158 $endDateTime = $props['endDateTime'];
1159
1160 /** @var Service $service */
1161 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1162
1163 /** @var Collection $providers */
1164 $providers = $slotsEntities->getProviders();
1165
1166 /** @var Collection $locations */
1167 $locations = $slotsEntities->getLocations();
1168
1169 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1170 $service,
1171 $props['extras']
1172 );
1173
1174 /** @var Provider $provider */
1175 foreach ($providers->getItems() as $provider) {
1176 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1177 /** @var Service $providerService */
1178 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1179
1180 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1181 continue;
1182 }
1183 }
1184
1185 $providerContainer = new Collection();
1186
1187 if ($provider->getTimeZone()) {
1188 $this->providerService->modifyProviderTimeZone(
1189 $provider,
1190 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1191 $startDateTime,
1192 $endDateTime
1193 );
1194 }
1195
1196 $start = $provider->getTimeZone() ?
1197 DateTimeService::getCustomDateTimeObjectInTimeZone(
1198 $startDateTime->format('Y-m-d H:i'),
1199 $provider->getTimeZone()->getValue()
1200 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1201
1202 $end = $provider->getTimeZone() ?
1203 DateTimeService::getCustomDateTimeObjectInTimeZone(
1204 $endDateTime->format('Y-m-d H:i'),
1205 $provider->getTimeZone()->getValue()
1206 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1207
1208 $providerContainer->addItem($provider, $provider->getId()->getValue());
1209
1210 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1211 $settings['limitPerEmployee']['numberOfApp'] : null;
1212
1213 $freeIntervals = $this->getFreeTime(
1214 $service,
1215 $props['locationId'],
1216 $locations,
1217 $providerContainer,
1218 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1219 [] : $settings['globalDaysOff'],
1220 $start,
1221 $end,
1222 $props['personsCount'],
1223 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1224 $settings['allowBookingIfNotMin'],
1225 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1226 !empty($settings['allowAdminBookOverApp']),
1227 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount]
1228 );
1229
1230 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1231 $service,
1232 $requiredTime,
1233 $freeIntervals,
1234 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1235 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1236 $settings['timeSlotLength'] ?: $requiredTime,
1237 $start,
1238 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1239 $settings['serviceDurationAsSlot'],
1240 $settings['bufferTimeInSlot'],
1241 true,
1242 $provider->getTimeZone() ?
1243 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName()
1244 );
1245 }
1246
1247 $freeSlots = [
1248 'available' => [],
1249 'occupied' => [],
1250 'appCount' => [],
1251 'duration' => $requiredTime / 60,
1252 ];
1253
1254 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1255 /** @var Provider $provider */
1256 $provider = $providers->getItem($providerKey);
1257
1258 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1259
1260 foreach (['available', 'occupied'] as $type) {
1261 if ($provider->getTimeZone()) {
1262 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1263 $providerSlots[$type],
1264 $provider->getTimeZone()->getValue()
1265 );
1266 }
1267
1268 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1269 foreach ($dateSlots as $timeKey => $slotData) {
1270 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1271 $freeSlots[$type][$dateKey][$timeKey] = [];
1272 }
1273
1274 foreach ($slotData as $item) {
1275 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1276 }
1277
1278 if (isset($freeSlots[$type][$dateKey])) {
1279 if (!$freeSlots[$type][$dateKey]) {
1280 unset($freeSlots[$type][$dateKey]);
1281 } else {
1282 ksort($freeSlots[$type][$dateKey]);
1283 }
1284 }
1285 }
1286 }
1287 }
1288 }
1289
1290 return $freeSlots;
1291 }
1292 }
1293