PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.20
Booking for Appointments and Events Calendar – Amelia v1.2.20
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
1177 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 (isset($weekDaysIntervals[$dayIndex]['busy'][$start]) &&
92 $weekDaysIntervals[$dayIndex]['busy'][$start][1] > $end
93 ) {
94 $end = $weekDaysIntervals[$dayIndex]['busy'][$start][1];
95 }
96
97 if (isset($intervals[$dateString]['occupied'][$start]) &&
98 $intervals[$dateString]['occupied'][$start][1] > $end
99 ) {
100 $end = $intervals[$dateString]['occupied'][$start][1];
101 }
102
103 return $end;
104 }
105
106 /**
107 * Split start and end in array of dates.
108 *
109 * @param DateTime $start
110 * @param DateTime $end
111 *
112 * @return array
113 */
114 private function getPeriodDates($start, $end)
115 {
116 /** @var DatePeriod $period */
117 $period = new DatePeriod(
118 $start->setTime(0, 0, 0),
119 new DateInterval('P1D'),
120 $end
121 );
122
123 $periodDates = [];
124
125 /** @var DateTime $date */
126 foreach ($period as $index => $date) {
127 $periodDates[] = $date->format('Y-m-d');
128 }
129
130 return $periodDates;
131 }
132
133 /** @noinspection MoreThanThreeArgumentsInspection */
134 /**
135 * get appointment intervals for provider.
136 *
137 * @param Provider $provider
138 * @param Collection $locations
139 * @param int $serviceId
140 * @param int $locationId
141 * @param int $personsCount
142 * @param boolean $bookIfPending
143 * @param array $weekDaysIntervals
144 * @param array $specialDaysIntervals
145 * @return array
146 * @throws InvalidArgumentException
147 */
148 private function getProviderAppointmentIntervals(
149 $provider,
150 $locations,
151 $serviceId,
152 $locationId,
153 $personsCount,
154 $bookIfPending,
155 &$weekDaysIntervals,
156 &$specialDaysIntervals
157 ) {
158 $intervals = [];
159
160 $specialDays = [];
161
162 foreach ($specialDaysIntervals as $specialDay) {
163 $specialDays = array_merge($specialDays, $specialDay['dates']);
164 }
165
166 /** @var Appointment $app */
167 foreach ($provider->getAppointmentList()->getItems() as $app) {
168 $occupiedStart = $provider->getTimeZone() ?
169 DateTimeService::getDateTimeObjectInTimeZone(
170 $app->getBookingStart()->getValue()->format('Y-m-d H:i'),
171 $provider->getTimeZone()->getValue()
172 ) : DateTimeService::getCustomDateTimeObject($app->getBookingStart()->getValue()->format('Y-m-d H:i'));
173
174 $occupiedEnd = $provider->getTimeZone() ?
175 DateTimeService::getDateTimeObjectInTimeZone(
176 $app->getBookingEnd()->getValue()->format('Y-m-d H:i'),
177 $provider->getTimeZone()->getValue()
178 ) : DateTimeService::getCustomDateTimeObject($app->getBookingEnd()->getValue()->format('Y-m-d H:i'));
179
180 if ($app->getServiceId()->getValue()) {
181 $occupiedStart->modify('-' . ($app->getService()->getTimeBefore() ? $app->getService()->getTimeBefore()->getValue() : 0) . ' seconds');
182
183 $occupiedEnd->modify('+' . ($app->getService()->getTimeAfter() ? $app->getService()->getTimeAfter()->getValue() : 0) . ' seconds');
184 }
185
186 $occupiedDateStart = $occupiedStart->format('Y-m-d');
187
188 $occupiedSecondsStart = $this->intervalService->getSeconds($occupiedStart->format('H:i') . ':00');
189
190 $occupiedSecondsEnd = $this->intervalService->getSeconds($occupiedEnd->format('H:i:s'));
191
192 if ($occupiedDateStart === $occupiedEnd->format('Y-m-d')) {
193 $intervals[$occupiedDateStart]['occupied'][$occupiedSecondsStart] = [
194 $occupiedSecondsStart,
195 $this->getModifiedEndInterval(
196 !array_key_exists($occupiedDateStart, $specialDays) ? $weekDaysIntervals : [],
197 $intervals,
198 $occupiedDateStart,
199 $occupiedSecondsStart,
200 $occupiedSecondsEnd
201 )
202 ];
203 } else {
204 $dates = $this->getPeriodDates($occupiedStart, $occupiedEnd);
205
206 $datesCount = sizeof($dates);
207
208 if ($datesCount === 1) {
209 $intervals[$dates[0]]['occupied'][$occupiedSecondsStart] = [
210 $occupiedSecondsStart,
211 $occupiedSecondsEnd === 0 ? 86400 : $occupiedSecondsEnd
212 ];
213 } else {
214 foreach ($dates as $index => $date) {
215 if ($index === 0) {
216 $intervals[$date]['occupied'][$occupiedSecondsStart] = [$occupiedSecondsStart, 86400];
217 } elseif ($index === $datesCount - 1) {
218 $modifiedEnd = $this->getModifiedEndInterval(
219 !array_key_exists($occupiedDateStart, $specialDays) ? $weekDaysIntervals : [],
220 $intervals,
221 $date,
222 0,
223 $occupiedSecondsEnd
224 );
225
226 $intervals[$date]['occupied'][0] = [
227 0,
228 $modifiedEnd === 0 ? 86400 : $modifiedEnd
229 ];
230 } else {
231 $intervals[$date]['occupied'][0] = [0, 86400];
232 }
233 }
234 }
235 }
236
237 $providerLocationId = $provider->getLocationId() ? $provider->getLocationId()->getValue() : null;
238
239 if ($app->getServiceId()->getValue() === $serviceId) {
240 $persons = 0;
241
242 /** @var CustomerBooking $booking */
243 foreach ($app->getBookings()->getItems() as $booking) {
244 $persons += $booking->getPersons()->getValue();
245 }
246
247 $status = $app->getStatus()->getValue();
248
249 $appLocationId = $app->getLocationId() ? $app->getLocationId()->getValue() : null;
250
251 $hasCapacity =
252 $personsCount !== null &&
253 ($persons + $personsCount) <= $app->getService()->getMaxCapacity()->getValue() &&
254 !($app->isFull() ? $app->isFull()->getValue() : false);
255
256 $hasLocation =
257 !$locationId ||
258 ($app->getLocationId() && $app->getLocationId()->getValue() === $locationId) ||
259 (!$app->getLocationId() && $providerLocationId === $locationId) ||
260 ($appLocationId &&
261 $appLocationId === $locationId &&
262 $locations->getItem($appLocationId)->getStatus()->getValue() === Status::VISIBLE) ||
263 (!$appLocationId && $providerLocationId &&
264 $locations->getItem($providerLocationId)->getStatus()->getValue() === Status::VISIBLE);
265
266 if (($hasLocation && $status === BookingStatus::APPROVED && $hasCapacity) ||
267 ($hasLocation && $status === BookingStatus::PENDING && ($bookIfPending || $hasCapacity))
268 ) {
269 $endDateTime = $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s');
270
271 $endDateTimeParts = explode(' ', $endDateTime);
272
273 $intervals[$occupiedDateStart]['available'][$app->getBookingStart()->getValue()->format('H:i')] =
274 [
275 'locationId' => $app->getLocationId() ?
276 $app->getLocationId()->getValue() : $providerLocationId,
277 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
278 'endDate' => $endDateTimeParts[0],
279 'endTime' => $endDateTimeParts[1],
280 'serviceId' => $serviceId,
281 ];
282 } else {
283 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
284 [
285 'locationId' => $app->getLocationId() ?
286 $app->getLocationId()->getValue() : $providerLocationId,
287 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
288 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
289 'serviceId' => $app->getServiceId()->getValue(),
290 ];
291 }
292 } elseif ($app->getServiceId()->getValue()) {
293 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
294 [
295 'locationId' => $app->getLocationId() ?
296 $app->getLocationId()->getValue() : $providerLocationId,
297 'places' => 0,
298 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
299 'serviceId' => $app->getServiceId()->getValue(),
300 ];
301 }
302 }
303
304 return $intervals;
305 }
306
307 /**
308 * get provider day off dates.
309 *
310 * @param Provider $provider
311 *
312 * @return array
313 * @throws Exception
314 */
315 private function getProviderDayOffDates($provider)
316 {
317 $dates = [];
318
319 /** @var DayOff $dayOff */
320 foreach ($provider->getDayOffList()->getItems() as $dayOff) {
321 $endDateCopy = clone $dayOff->getEndDate()->getValue();
322
323 $dayOffPeriod = new DatePeriod(
324 $dayOff->getStartDate()->getValue(),
325 new DateInterval('P1D'),
326 $endDateCopy->modify('+1 day')
327 );
328
329 /** @var DateTime $date */
330 foreach ($dayOffPeriod as $date) {
331 $dateFormatted = $dayOff->getRepeat()->getValue() ? $date->format('m-d') : $date->format('Y-m-d');
332
333 $dates[$dateFormatted] = $dateFormatted;
334 }
335 }
336
337 return $dates;
338 }
339
340 /**
341 * get available appointment intervals.
342 *
343 * @param array $availableIntervals
344 * @param array $unavailableIntervals
345 *
346 * @return array
347 */
348 private function getAvailableIntervals(&$availableIntervals, $unavailableIntervals)
349 {
350 $parsedAvailablePeriod = [];
351
352 ksort($availableIntervals);
353 ksort($unavailableIntervals);
354
355 foreach ($availableIntervals as $available) {
356 $parsedAvailablePeriod[] = $available;
357
358 foreach ($unavailableIntervals as $unavailable) {
359 if ($parsedAvailablePeriod) {
360 $lastAvailablePeriod = $parsedAvailablePeriod[sizeof($parsedAvailablePeriod) - 1];
361
362 if ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
363 // unavailable interval is inside available interval
364 $fixedPeriod = array_pop($parsedAvailablePeriod);
365
366 if ($fixedPeriod[0] !== $unavailable[0]) {
367 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
368 }
369
370 if ($unavailable[1] !== $fixedPeriod[1]) {
371 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
372 }
373 } elseif ($unavailable[0] <= $lastAvailablePeriod[0] && $unavailable[1] >= $lastAvailablePeriod[1]) {
374 // available interval is inside unavailable interval
375 array_pop($parsedAvailablePeriod);
376 } elseif ($unavailable[0] <= $lastAvailablePeriod[0] && $unavailable[1] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
377 // unavailable interval intersect start of available interval
378 $fixedPeriod = array_pop($parsedAvailablePeriod);
379
380 if ($unavailable[1] !== $fixedPeriod[1]) {
381 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
382 }
383 } elseif ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[0] <= $lastAvailablePeriod[1] && $unavailable[1] >= $lastAvailablePeriod[1]) {
384 // unavailable interval intersect end of available interval
385 $fixedPeriod = array_pop($parsedAvailablePeriod);
386
387 if ($fixedPeriod[0] !== $unavailable[0]) {
388 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
389 }
390 }
391 }
392 }
393 }
394
395 return $parsedAvailablePeriod;
396 }
397
398 /**
399 * @param Service $service
400 * @param Provider $provider
401 * @param int $personsCount
402 *
403 * @return bool
404 *
405 * @throws Exception
406 */
407 private function getOnlyAppointmentsSlots($service, $provider, $personsCount)
408 {
409 $getOnlyAppointmentsSlots = false;
410
411 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
412 /** @var Service $providerService */
413 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
414
415 if ($personsCount < $providerService->getMinCapacity()->getValue()) {
416 $getOnlyAppointmentsSlots = true;
417 }
418 }
419
420 return $getOnlyAppointmentsSlots;
421 }
422
423 /** @noinspection MoreThanThreeArgumentsInspection */
424 /**
425 * @param Service $service
426 * @param int $locationId
427 * @param Collection $providers
428 * @param Collection $locations
429 * @param array $globalDaysOffDates
430 * @param DateTime $startDateTime
431 * @param DateTime $endDateTime
432 * @param int $personsCount
433 * @param boolean $bookIfPending
434 * @param boolean $bookIfNotMin
435 * @param boolean $bookAfterMin
436 * @param array $appointmentsCount
437 *
438 * @return array
439 * @throws Exception
440 */
441 private function getFreeTime(
442 Service $service,
443 $locationId,
444 Collection $locations,
445 Collection $providers,
446 array $globalDaysOffDates,
447 DateTime $startDateTime,
448 DateTime $endDateTime,
449 $personsCount,
450 $bookIfPending,
451 $bookIfNotMin,
452 $bookAfterMin,
453 $appointmentsCount
454 ) {
455
456 $weekDayIntervals = [];
457
458 $appointmentIntervals = [];
459
460 $daysOffDates = [];
461
462 $specialDayIntervals = [];
463
464 $getOnlyAppointmentsSlots = [];
465
466 $serviceId = $service->getId()->getValue();
467
468 /** @var Provider $provider */
469 foreach ($providers->getItems() as $provider) {
470 $providerId = $provider->getId()->getValue();
471
472 $getOnlyAppointmentsSlots[$providerId] = $bookIfNotMin && $bookAfterMin ? $this->getOnlyAppointmentsSlots(
473 $service,
474 $provider,
475 $personsCount
476 ) : false;
477
478 $daysOffDates[$providerId] = $this->getProviderDayOffDates($provider);
479
480 $weekDayIntervals[$providerId] = $this->scheduleService->getProviderWeekDaysIntervals(
481 $provider,
482 $locations,
483 $locationId,
484 $serviceId
485 );
486
487 $specialDayIntervals[$providerId] = $this->scheduleService->getProviderSpecialDayIntervals(
488 $provider,
489 $locations,
490 $locationId,
491 $serviceId
492 );
493
494 $appointmentIntervals[$providerId] = $this->getProviderAppointmentIntervals(
495 $provider,
496 $locations,
497 $serviceId,
498 $locationId,
499 $personsCount,
500 $bookIfPending,
501 $weekDayIntervals[$providerId],
502 $specialDayIntervals[$providerId]
503 );
504 }
505
506 $freeDateIntervals = [];
507
508 foreach ($appointmentIntervals as $providerKey => $providerDates) {
509 foreach ((array)$providerDates as $dateKey => $dateIntervals) {
510 $dayIndex = DateTimeService::getDayIndex($dateKey);
511
512 $specialDayDateKey = null;
513
514 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
515 if (array_key_exists($dateKey, $specialDays['dates'])) {
516 $specialDayDateKey = $specialDayKey;
517 break;
518 }
519 }
520
521 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
522 // get free intervals if it is special day
523 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
524 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
525 $dateIntervals['occupied']
526 );
527 } elseif (isset($weekDayIntervals[$providerKey][$dayIndex]['free']) && !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])) {
528 // get free intervals if it is working day
529 $unavailableIntervals =
530 $weekDayIntervals[$providerKey][$dayIndex]['busy'] + $dateIntervals['occupied'];
531
532 $intersectedTimes = array_intersect(
533 array_keys($weekDayIntervals[$providerKey][$dayIndex]['busy']),
534 array_keys($dateIntervals['occupied'])
535 );
536
537 foreach ($intersectedTimes as $time) {
538 $unavailableIntervals[$time] =
539 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] >
540 $dateIntervals['occupied'][$time] ?
541 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] :
542 $dateIntervals['occupied'][$time];
543 }
544
545 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
546 $weekDayIntervals[$providerKey][$dayIndex]['free'],
547 $unavailableIntervals
548 );
549 }
550 }
551 }
552
553 $startDateTime = clone $startDateTime;
554
555 $startDateTime->setTime(0, 0);
556
557 $endDateTime = clone $endDateTime;
558
559 $endDateTime->modify('+1 day')->setTime(0, 0);
560
561 // create calendar
562 $period = new DatePeriod(
563 $startDateTime,
564 new DateInterval('P1D'),
565 $endDateTime
566 );
567
568 $calendar = [];
569
570 /** @var DateTime $day */
571 foreach ($period as $day) {
572 $currentDate = $day->format('Y-m-d');
573 $dayIndex = (int)$day->format('N');
574
575 $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) ||
576 array_key_exists($day->format('m-d'), $globalDaysOffDates);
577
578 if (!$isGlobalDayOff) {
579 foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) {
580 $isProviderDayOff = array_key_exists($currentDate, $daysOffDates[$providerKey]) ||
581 array_key_exists($day->format('m-d'), $daysOffDates[$providerKey]);
582
583 $specialDayDateKey = null;
584
585 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
586 if (array_key_exists($currentDate, $specialDays['dates'])) {
587 $specialDayDateKey = $specialDayKey;
588 break;
589 }
590 }
591
592 if (!$isProviderDayOff) {
593 if (!empty($appointmentsCount['limitCount']) && !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
594 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']) {
595 continue;
596 }
597
598 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
599 // get date intervals if there are appointments (special or working day)
600 $calendar[$currentDate][$providerKey] = [
601 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
602 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
603 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
604 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
605 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
606 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
607 ];
608 } else {
609 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
610 // get date intervals if it is special day with out appointments
611 $calendar[$currentDate][$providerKey] = [
612 'slots' => [],
613 'full' => [],
614 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
615 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
616 ];
617 } elseif (isset($weekDayIntervals[$providerKey][$dayIndex]) && !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])) {
618 // get date intervals if it is working day without appointments
619 $calendar[$currentDate][$providerKey] = [
620 'slots' => [],
621 'full' => [],
622 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $weekDayIntervals[$providerKey][$dayIndex]['free'],
623 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
624 ];
625 }
626 }
627 }
628 }
629 }
630 }
631
632 return $calendar;
633 }
634
635 /** @noinspection MoreThanThreeArgumentsInspection */
636 /**
637 * @param Service $service
638 * @param int $requiredTime
639 * @param array $freeIntervals
640 * @param array $resourcedIntervals
641 * @param int $slotLength
642 * @param DateTime $startDateTime
643 * @param bool $serviceDurationAsSlot
644 * @param bool $bufferTimeInSlot
645 * @param bool $isFrontEndBooking
646 * @param String $timeZone
647 *
648 * @return array
649 */
650 private function getAppointmentFreeSlots(
651 $service,
652 $requiredTime,
653 &$freeIntervals,
654 $resourcedIntervals,
655 $slotLength,
656 $startDateTime,
657 $serviceDurationAsSlot,
658 $bufferTimeInSlot,
659 $isFrontEndBooking,
660 $timeZone
661 ) {
662 $availableResult = [];
663
664 $occupiedResult = [];
665
666 $realRequiredTime = $requiredTime -
667 $service->getTimeBefore()->getValue() -
668 $service->getTimeAfter()->getValue();
669
670 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
671 $requiredTime = $requiredTime -
672 $service->getTimeBefore()->getValue() -
673 $service->getTimeAfter()->getValue();
674 }
675
676 $currentDateTime = DateTimeService::getNowDateTimeObject();
677
678 $currentDateString = $currentDateTime->format('Y-m-d');
679
680 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
681
682 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
683
684 $currentDateFormatted = $currentDateTime->format('Y-m-d');
685
686 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
687
688 $startDateFormatted = $startDateTime->format('Y-m-d');
689
690 $bookingLength = $serviceDurationAsSlot && $isFrontEndBooking ? $requiredTime : $slotLength;
691
692 $appCount = [];
693
694 foreach ($freeIntervals as $dateKey => $dateProviders) {
695 foreach ((array)$dateProviders as $providerKey => $provider) {
696 foreach ((array)$provider['intervals'] as $timePeriod) {
697 if ($timePeriod[1] === 86400) {
698 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
699 $dateKey . ' 00:00:00',
700 $timeZone
701 )->modify('+1 days')->format('Y-m-d');
702
703 if (isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
704 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
705 ) {
706 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
707
708 $timePeriod[1] += ($realRequiredTime <= $nextDayInterval ? $realRequiredTime : $nextDayInterval);
709 }
710 }
711
712 if (!$bufferTimeInSlot && $serviceDurationAsSlot) {
713 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
714 }
715
716 $customerTimeStart = $timePeriod[0] + $service->getTimeBefore()->getValue();
717
718 $providerTimeStart = $customerTimeStart - $service->getTimeBefore()->getValue();
719
720 $numberOfSlots = (int)(floor(($timePeriod[1] - $providerTimeStart - $requiredTime) / $bookingLength) + 1);
721
722 $inspectResourceIndexes = [];
723
724 if (isset($resourcedIntervals[$dateKey])) {
725 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
726 if (array_intersect(
727 $timePeriod[2],
728 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
729 )) {
730 $inspectResourceIndexes[] = $resourceIndex;
731 }
732 }
733 }
734
735 $providerPeriodSlots = [];
736
737 $achievedLength = 0;
738
739 for ($i = 0; $i < $numberOfSlots; $i++) {
740 $achievedLength += $bookingLength;
741
742 $timeSlot = $customerTimeStart + $i * $bookingLength;
743
744 if (($startDateFormatted !== $dateKey && ($serviceDurationAsSlot && !$bufferTimeInSlot ? $timeSlot <= $timePeriod[1] - $requiredTime : true)) ||
745 ($startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot) ||
746 ($startDateFormatted === $currentDateFormatted && $startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot && $currentTimeInSeconds < $timeSlot)
747 ) {
748 $timeSlotEnd = $timeSlot + $bookingLength;
749
750 $filteredLocationsIds = $timePeriod[2];
751
752 foreach ($inspectResourceIndexes as $resourceIndex) {
753 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
754 if (($start >= $timeSlot && $start < $timeSlotEnd) ||
755 ($end > $timeSlot && $end <= $timeSlotEnd) ||
756 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
757 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
758 ) {
759 $filteredLocationsIds = array_diff(
760 $filteredLocationsIds,
761 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
762 );
763
764 if (!$filteredLocationsIds) {
765 if ($achievedLength < $requiredTime) {
766 $providerPeriodSlots = [];
767
768 $achievedLength = 0;
769 }
770
771 continue 3;
772 }
773
774 $removedLocationsIds = array_diff(
775 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
776 $filteredLocationsIds
777 );
778
779 if ($removedLocationsIds && $achievedLength < $requiredTime) {
780 $parsedPeriodSlots = [];
781
782 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
783 if ($start >= $previousTimeSlot &&
784 $start < $previousTimeSlot + $requiredTime
785 ) {
786 foreach ($periodSlotData as $data) {
787 if (!in_array($data[1], $removedLocationsIds)) {
788 $parsedPeriodSlots[$previousTimeSlot][] = $data;
789 }
790 }
791 } else {
792 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
793 }
794 }
795
796 $providerPeriodSlots = $parsedPeriodSlots;
797 }
798 }
799 }
800 }
801
802 if (!$timePeriod[2]) {
803 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
804 } else if ($filteredLocationsIds) {
805 foreach ($filteredLocationsIds as $locationId) {
806 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
807 }
808 }
809 }
810 }
811
812 foreach ($providerPeriodSlots as $timeSlot => $data) {
813 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
814 . sprintf('%02d', floor(($timeSlot / 60) % 60));
815
816 $availableResult[$dateKey][$time] = $data;
817 }
818 }
819
820 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
821 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
822
823 if ($currentDateString === $dateKey &&
824 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
825 ) {
826 continue;
827 }
828
829 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
830
831 $newEndInSeconds = $startInSeconds + $realRequiredTime;
832
833 if ($newEndInSeconds !== 86400 &&
834 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
835 ) {
836 if ($dateKey !== $appointmentData['endDate']) {
837 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
838 $dateKey . ' 00:00:00',
839 $timeZone
840 )->modify('+1 days')->format('Y-m-d');
841
842 if (!isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
843 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
844 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
845 ) {
846 continue;
847 }
848 } elseif ($newEndInSeconds > 86400) {
849 $nextIntervalIsValid = false;
850
851 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
852 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
853 $nextIntervalIsValid = true;
854
855 break;
856 }
857 }
858
859 if (!$nextIntervalIsValid) {
860 continue;
861 }
862
863 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
864 $dateKey . ' 00:00:00',
865 $timeZone
866 )->modify('+1 days')->format('Y-m-d');
867
868 if (!isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
869 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
870 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
871 ) {
872 continue;
873 }
874 } else {
875 $nextIntervalIsValid = false;
876
877 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
878 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
879 $nextIntervalIsValid = true;
880
881 break;
882 }
883 }
884
885 if (!$nextIntervalIsValid) {
886 continue;
887 }
888 }
889 }
890
891 $availableResult[$dateKey][$appointmentTime][] = [
892 $providerKey,
893 $appointmentData['locationId'],
894 $appointmentData['places'],
895 $appointmentData['serviceId']
896 ];
897 }
898
899 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
900 $occupiedResult[$dateKey][$appointmentTime][] = [
901 $providerKey,
902 $appointmentData['locationId'],
903 $appointmentData['places'],
904 $appointmentData['serviceId']
905 ];
906 }
907
908 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
909 }
910 }
911
912 return [
913 'available' => $availableResult,
914 'occupied' => $occupiedResult,
915 'appCount' => $appCount
916 ];
917 }
918
919 /**
920 * @param array $slots
921 * @param string $timeZone
922 *
923 * @return array
924 * @throws Exception
925 */
926 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
927 {
928 $convertedProviderSlots = [];
929
930 foreach ($slots as $slotDate => $slotTimes) {
931 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
932 $convertedSlotParts = explode(
933 ' ',
934 DateTimeService::getDateTimeObjectInTimeZone(
935 $slotDate . ' ' . $slotTime,
936 $timeZone
937 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
938 );
939
940 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
941 }
942 }
943
944 return $convertedProviderSlots;
945 }
946
947
948 /**
949 * @param Collection $appointments
950 * @param int $excludeAppointmentId
951 *
952 * @return array
953 * @throws Exception
954 */
955 public function getAppointmentCount($appointments, $excludeAppointmentId)
956 {
957 $appCount = [];
958
959 /** @var Appointment $appointment */
960 foreach ($appointments->getItems() as $appointment) {
961 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
962 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
963 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
964 } else {
965 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
966 }
967 }
968 }
969
970 return $appCount;
971 }
972
973 /** @noinspection MoreThanThreeArgumentsInspection */
974 /**
975 * @param array $settings
976 * @param array $props
977 * @param SlotsEntities $slotsEntities
978 * @param Collection $appointments
979 *
980 * @return array
981 * @throws Exception
982 */
983 public function getSlots($settings, $props, $slotsEntities, $appointments)
984 {
985 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
986
987 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
988 $this->resourceService->manageResources(
989 $slotsEntities->getResources(),
990 $appointments,
991 $slotsEntities->getLocations(),
992 $slotsEntities->getServices()->getItem($props['serviceId']),
993 $slotsEntities->getProviders(),
994 $props['locationId'],
995 $props['excludeAppointmentId'],
996 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
997 ) : [];
998
999 $continuousAppointments = $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1000
1001 $this->providerService->addAppointmentsToAppointmentList(
1002 $slotsEntities->getProviders(),
1003 $appointments,
1004 $settings['isGloballyBusySlot']
1005 );
1006
1007 return $this->getCalculatedFreeSlots(
1008 $settings,
1009 $props,
1010 $slotsEntities,
1011 $resourcedLocationsIntervals,
1012 $continuousAppointments,
1013 $appointmentsCount
1014 );
1015 }
1016
1017 /** @noinspection MoreThanThreeArgumentsInspection */
1018 /**
1019 * @param array $settings
1020 * @param array $props
1021 * @param SlotsEntities $slotsEntities
1022 * @param array $resourcedLocationsIntervals
1023 * @param array $continuousAppointments
1024 * @param array $appointmentsCount
1025 *
1026 * @return array
1027 * @throws Exception
1028 */
1029 private function getCalculatedFreeSlots(
1030 $settings,
1031 $props,
1032 $slotsEntities,
1033 $resourcedLocationsIntervals,
1034 $continuousAppointments,
1035 $appointmentsCount
1036 ) {
1037 $freeProvidersSlots = [];
1038
1039 /** @var DateTime $startDateTime */
1040 $startDateTime = $props['startDateTime'];
1041
1042 /** @var DateTime $endDateTime */
1043 $endDateTime = $props['endDateTime'];
1044
1045 /** @var Service $service */
1046 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1047
1048 /** @var Collection $providers */
1049 $providers = $slotsEntities->getProviders();
1050
1051 /** @var Collection $locations */
1052 $locations = $slotsEntities->getLocations();
1053
1054 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1055 $service,
1056 $props['extras']
1057 );
1058
1059 /** @var Provider $provider */
1060 foreach ($providers->getItems() as $provider) {
1061 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1062 /** @var Service $providerService */
1063 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1064
1065 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1066 continue;
1067 }
1068 }
1069
1070 $providerContainer = new Collection();
1071
1072 if ($provider->getTimeZone()) {
1073 $this->providerService->modifyProviderTimeZone(
1074 $provider,
1075 $settings['globalDaysOff'],
1076 $startDateTime,
1077 $endDateTime
1078 );
1079 }
1080
1081 $start = $provider->getTimeZone() ?
1082 DateTimeService::getCustomDateTimeObjectInTimeZone(
1083 $startDateTime->format('Y-m-d H:i'),
1084 $provider->getTimeZone()->getValue()
1085 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1086
1087 $end = $provider->getTimeZone() ?
1088 DateTimeService::getCustomDateTimeObjectInTimeZone(
1089 $endDateTime->format('Y-m-d H:i'),
1090 $provider->getTimeZone()->getValue()
1091 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1092
1093 $providerContainer->addItem($provider, $provider->getId()->getValue());
1094
1095 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1096 $settings['limitPerEmployee']['numberOfApp'] : null;
1097
1098 $freeIntervals = $this->getFreeTime(
1099 $service,
1100 $props['locationId'],
1101 $locations,
1102 $providerContainer,
1103 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1104 [] : $settings['globalDaysOff'],
1105 $start,
1106 $end,
1107 $props['personsCount'],
1108 $settings['allowBookingIfPending'],
1109 $settings['allowBookingIfNotMin'],
1110 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1111 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount]
1112 );
1113
1114 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1115 $service,
1116 $requiredTime,
1117 $freeIntervals,
1118 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1119 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1120 $settings['timeSlotLength'] ?: $requiredTime,
1121 $start,
1122 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1123 $settings['serviceDurationAsSlot'],
1124 $settings['bufferTimeInSlot'],
1125 true,
1126 $provider->getTimeZone() ?
1127 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName()
1128 );
1129 }
1130
1131 $freeSlots = [
1132 'available' => [],
1133 'occupied' => [],
1134 'continuousAppointments' => $continuousAppointments[0],
1135 'appCount' => []
1136 ];
1137
1138 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1139 /** @var Provider $provider */
1140 $provider = $providers->getItem($providerKey);
1141
1142 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1143
1144 foreach (['available', 'occupied'] as $type) {
1145 if ($provider->getTimeZone()) {
1146 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1147 $providerSlots[$type],
1148 $provider->getTimeZone()->getValue()
1149 );
1150 }
1151
1152 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1153 foreach ($dateSlots as $timeKey => $slotData) {
1154 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1155 $freeSlots[$type][$dateKey][$timeKey] = [];
1156 }
1157
1158 foreach ($slotData as $item) {
1159 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1160 }
1161
1162 if (isset($freeSlots[$type][$dateKey])) {
1163 if (!$freeSlots[$type][$dateKey]) {
1164 unset($freeSlots[$type][$dateKey]);
1165 } else {
1166 ksort($freeSlots[$type][$dateKey]);
1167 }
1168 }
1169 }
1170 }
1171 }
1172 }
1173
1174 return $freeSlots;
1175 }
1176 }
1177