PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.11
Booking for Appointments and Events Calendar – Amelia v1.2.11
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
1175 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 > $endInSeconds && $newEndInSeconds !== 86400) {
834 if ($dateKey !== $appointmentData['endDate']) {
835 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
836 $dateKey . ' 00:00:00',
837 $timeZone
838 )->modify('+1 days')->format('Y-m-d');
839
840 if (!isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
841 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
842 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
843 ) {
844 continue;
845 }
846 } elseif ($newEndInSeconds > 86400) {
847 $nextIntervalIsValid = false;
848
849 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
850 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
851 $nextIntervalIsValid = true;
852
853 break;
854 }
855 }
856
857 if (!$nextIntervalIsValid) {
858 continue;
859 }
860
861 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
862 $dateKey . ' 00:00:00',
863 $timeZone
864 )->modify('+1 days')->format('Y-m-d');
865
866 if (!isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
867 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
868 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
869 ) {
870 continue;
871 }
872 } else {
873 $nextIntervalIsValid = false;
874
875 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
876 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
877 $nextIntervalIsValid = true;
878
879 break;
880 }
881 }
882
883 if (!$nextIntervalIsValid) {
884 continue;
885 }
886 }
887 }
888
889 $availableResult[$dateKey][$appointmentTime][] = [
890 $providerKey,
891 $appointmentData['locationId'],
892 $appointmentData['places'],
893 $appointmentData['serviceId']
894 ];
895 }
896
897 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
898 $occupiedResult[$dateKey][$appointmentTime][] = [
899 $providerKey,
900 $appointmentData['locationId'],
901 $appointmentData['places'],
902 $appointmentData['serviceId']
903 ];
904 }
905
906 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
907 }
908 }
909
910 return [
911 'available' => $availableResult,
912 'occupied' => $occupiedResult,
913 'appCount' => $appCount
914 ];
915 }
916
917 /**
918 * @param array $slots
919 * @param string $timeZone
920 *
921 * @return array
922 * @throws Exception
923 */
924 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
925 {
926 $convertedProviderSlots = [];
927
928 foreach ($slots as $slotDate => $slotTimes) {
929 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
930 $convertedSlotParts = explode(
931 ' ',
932 DateTimeService::getDateTimeObjectInTimeZone(
933 $slotDate . ' ' . $slotTime,
934 $timeZone
935 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
936 );
937
938 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
939 }
940 }
941
942 return $convertedProviderSlots;
943 }
944
945
946 /**
947 * @param Collection $appointments
948 * @param int $excludeAppointmentId
949 *
950 * @return array
951 * @throws Exception
952 */
953 public function getAppointmentCount($appointments, $excludeAppointmentId)
954 {
955 $appCount = [];
956
957 /** @var Appointment $appointment */
958 foreach ($appointments->getItems() as $appointment) {
959 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
960 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
961 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
962 } else {
963 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
964 }
965 }
966 }
967
968 return $appCount;
969 }
970
971 /** @noinspection MoreThanThreeArgumentsInspection */
972 /**
973 * @param array $settings
974 * @param array $props
975 * @param SlotsEntities $slotsEntities
976 * @param Collection $appointments
977 *
978 * @return array
979 * @throws Exception
980 */
981 public function getSlots($settings, $props, $slotsEntities, $appointments)
982 {
983 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
984
985 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
986 $this->resourceService->manageResources(
987 $slotsEntities->getResources(),
988 $appointments,
989 $slotsEntities->getLocations(),
990 $slotsEntities->getServices()->getItem($props['serviceId']),
991 $slotsEntities->getProviders(),
992 $props['locationId'],
993 $props['excludeAppointmentId'],
994 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
995 ) : [];
996
997 $continuousAppointments = $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
998
999 $this->providerService->addAppointmentsToAppointmentList(
1000 $slotsEntities->getProviders(),
1001 $appointments,
1002 $settings['isGloballyBusySlot']
1003 );
1004
1005 return $this->getCalculatedFreeSlots(
1006 $settings,
1007 $props,
1008 $slotsEntities,
1009 $resourcedLocationsIntervals,
1010 $continuousAppointments,
1011 $appointmentsCount
1012 );
1013 }
1014
1015 /** @noinspection MoreThanThreeArgumentsInspection */
1016 /**
1017 * @param array $settings
1018 * @param array $props
1019 * @param SlotsEntities $slotsEntities
1020 * @param array $resourcedLocationsIntervals
1021 * @param array $continuousAppointments
1022 * @param array $appointmentsCount
1023 *
1024 * @return array
1025 * @throws Exception
1026 */
1027 private function getCalculatedFreeSlots(
1028 $settings,
1029 $props,
1030 $slotsEntities,
1031 $resourcedLocationsIntervals,
1032 $continuousAppointments,
1033 $appointmentsCount
1034 ) {
1035 $freeProvidersSlots = [];
1036
1037 /** @var DateTime $startDateTime */
1038 $startDateTime = $props['startDateTime'];
1039
1040 /** @var DateTime $endDateTime */
1041 $endDateTime = $props['endDateTime'];
1042
1043 /** @var Service $service */
1044 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1045
1046 /** @var Collection $providers */
1047 $providers = $slotsEntities->getProviders();
1048
1049 /** @var Collection $locations */
1050 $locations = $slotsEntities->getLocations();
1051
1052 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1053 $service,
1054 $props['extras']
1055 );
1056
1057 /** @var Provider $provider */
1058 foreach ($providers->getItems() as $provider) {
1059 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1060 /** @var Service $providerService */
1061 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1062
1063 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1064 continue;
1065 }
1066 }
1067
1068 $providerContainer = new Collection();
1069
1070 if ($provider->getTimeZone()) {
1071 $this->providerService->modifyProviderTimeZone(
1072 $provider,
1073 $settings['globalDaysOff'],
1074 $startDateTime,
1075 $endDateTime
1076 );
1077 }
1078
1079 $start = $provider->getTimeZone() ?
1080 DateTimeService::getCustomDateTimeObjectInTimeZone(
1081 $startDateTime->format('Y-m-d H:i'),
1082 $provider->getTimeZone()->getValue()
1083 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1084
1085 $end = $provider->getTimeZone() ?
1086 DateTimeService::getCustomDateTimeObjectInTimeZone(
1087 $endDateTime->format('Y-m-d H:i'),
1088 $provider->getTimeZone()->getValue()
1089 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1090
1091 $providerContainer->addItem($provider, $provider->getId()->getValue());
1092
1093 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1094 $settings['limitPerEmployee']['numberOfApp'] : null;
1095
1096 $freeIntervals = $this->getFreeTime(
1097 $service,
1098 $props['locationId'],
1099 $locations,
1100 $providerContainer,
1101 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1102 [] : $settings['globalDaysOff'],
1103 $start,
1104 $end,
1105 $props['personsCount'],
1106 $settings['allowBookingIfPending'],
1107 $settings['allowBookingIfNotMin'],
1108 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1109 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount]
1110 );
1111
1112 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1113 $service,
1114 $requiredTime,
1115 $freeIntervals,
1116 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1117 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1118 $settings['timeSlotLength'] ?: $requiredTime,
1119 $start,
1120 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1121 $settings['serviceDurationAsSlot'],
1122 $settings['bufferTimeInSlot'],
1123 true,
1124 $provider->getTimeZone() ?
1125 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName()
1126 );
1127 }
1128
1129 $freeSlots = [
1130 'available' => [],
1131 'occupied' => [],
1132 'continuousAppointments' => $continuousAppointments[0],
1133 'appCount' => []
1134 ];
1135
1136 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1137 /** @var Provider $provider */
1138 $provider = $providers->getItem($providerKey);
1139
1140 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1141
1142 foreach (['available', 'occupied'] as $type) {
1143 if ($provider->getTimeZone()) {
1144 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1145 $providerSlots[$type],
1146 $provider->getTimeZone()->getValue()
1147 );
1148 }
1149
1150 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1151 foreach ($dateSlots as $timeKey => $slotData) {
1152 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1153 $freeSlots[$type][$dateKey][$timeKey] = [];
1154 }
1155
1156 foreach ($slotData as $item) {
1157 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1158 }
1159
1160 if (isset($freeSlots[$type][$dateKey])) {
1161 if (!$freeSlots[$type][$dateKey]) {
1162 unset($freeSlots[$type][$dateKey]);
1163 } else {
1164 ksort($freeSlots[$type][$dateKey]);
1165 }
1166 }
1167 }
1168 }
1169 }
1170 }
1171
1172 return $freeSlots;
1173 }
1174 }
1175