getSpecialDayList()->keys() as $specialDayKey) { /** @var SpecialDay $specialDay */ $specialDay = $provider->getSpecialDayList()->getItem($specialDayKey); $specialDays = []; $endDateCopy = clone $specialDay->getEndDate()->getValue(); $specialDaysPeriod = new \DatePeriod( $specialDay->getStartDate()->getValue(), new \DateInterval('P1D'), $endDateCopy->modify('+1 day') ); /** @var \DateTime $day */ foreach ($specialDaysPeriod as $day) { $specialDays[] = $day->format('Y-m-d'); } $minStartPeriod = null; $maxEndPeriod = null; $availableDateIntervals = []; foreach ((array)$specialDay->getPeriodList()->keys() as $specialDayPeriodKey) { /** @var SpecialDayPeriod $specialDayPeriod */ $specialDayPeriod = $specialDay->getPeriodList()->getItem($specialDayPeriodKey); $hasPeriodService = $specialDayPeriod->getPeriodServiceList()->length() === 0; foreach ((array)$specialDayPeriod->getPeriodServiceList()->keys() as $specialDayPeriodServiceKey) { /** @var SpecialDayPeriodService $specialDayPeriodService */ $specialDayPeriodService = $specialDayPeriod->getPeriodServiceList()->getItem($specialDayPeriodServiceKey); if ($specialDayPeriodService->getServiceId()->getValue() === $serviceId) { $hasPeriodService = true; } } if ($hasPeriodService) { $startPeriod = $this->getSeconds( $specialDayPeriod->getStartTime()->getValue()->format('H:i:s') ); $endPeriod = $this->getSeconds( $specialDayPeriod->getEndTime()->getValue()->format('H:i:s') === '00:00:00' ? '24:00:00' : $specialDayPeriod->getEndTime()->getValue()->format('H:i:s') ); if ($minStartPeriod === null || $startPeriod < $minStartPeriod) { $minStartPeriod = $startPeriod; } if ($maxEndPeriod === null || $endPeriod > $maxEndPeriod) { $maxEndPeriod = $endPeriod; } $availableDateIntervals[$startPeriod] = [ $startPeriod, $endPeriod ]; } } $availableIntervals[] = [ 'dates' => $specialDays, 'startTime' => $minStartPeriod, 'endTime' => $maxEndPeriod, 'intervals' => $this->getFreeIntervals( $availableDateIntervals, $minStartPeriod, $maxEndPeriod ) ]; } return $availableIntervals; } /** * get weekdays timeout intervals for provider. * * @param Provider $provider * * @return array * @throws InvalidArgumentException * @throws \Exception */ private function getProviderOccupiedTimeIntervals($provider) { $intervals = []; foreach ((array)$provider->getWeekDayList()->keys() as $weekDayKey) { /** @var WeekDay $weekDay */ $weekDay = $provider->getWeekDayList()->getItem($weekDayKey); $dayIndex = $weekDay->getDayIndex()->getValue(); $intervals[$dayIndex] = $this->getWeekDayIntervals($weekDay); } return $intervals; } /** * get appointment intervals for provider. * * @param Provider $provider * * @return array * @throws InvalidArgumentException */ private function getProviderAppointmentIntervals($provider) { $intervals = []; $appointmentsArray = []; // group appointments by date foreach ((array)$provider->getAppointmentList()->keys() as $appointmentKey) { /** @var Appointment $appointment */ $appointment = $provider->getAppointmentList()->getItem($appointmentKey); $appointmentDate = $appointment->getBookingStart()->getValue()->format('Y-m-d'); $appointmentsArray[$appointmentDate][] = $appointment; } foreach ($appointmentsArray as $appointmentsDateKey => $appointments) { $intervals[$appointmentsDateKey] = [ 'intervals' => $this->getDateAppointmentsIntervals($appointments), 'appointments' => $appointments, ]; } return $intervals; } /** * get provider day off dates. * * @param Provider $provider * * @return array * @throws InvalidArgumentException * @throws \Exception */ private function getProviderDayOffDates($provider) { $dates = []; foreach ((array)$provider->getDayOffList()->keys() as $dayOffKey) { /** @var DayOff $dayOff */ $dayOff = $provider->getDayOffList()->getItem($dayOffKey); $dayOffPeriod = new \DatePeriod( $dayOff->getStartDate()->getValue(), new \DateInterval('P1D'), $dayOff->getEndDate()->getValue()->modify('+1 day') ); /** @var \DateTime $date */ foreach ($dayOffPeriod as $date) { if ($dayOff->getRepeat()->getValue()) { $dateFormatted = $date->format('m-d'); $dates[$dateFormatted] = $dateFormatted; } else { $dateFormatted = $date->format('Y-m-d'); $dates[$dateFormatted] = $dateFormatted; } } } return $dates; } /** @noinspection MoreThanThreeArgumentsInspection */ /** * get provider appointment intervals. * * @param Service $service * @param array $appointments * @param int $personsCount * @param boolean $bookIfPending * * @return array */ private function getProviderAppointmentBookingStartTimes($service, $appointments, $personsCount, $bookIfPending) { $bookingStartTimes = []; foreach ((array)$appointments as $appointment) { if ($appointment->getServiceId()->getValue() === $service->getId()->getValue()) { $persons = 0; foreach ((array)$appointment->getBookings()->keys() as $bookingKey) { $persons += $appointment->getBookings()->getItem($bookingKey)->getPersons()->getValue(); } $status = $appointment->getStatus()->getValue(); $hasCapacity = $persons + $personsCount <= $appointment->getService()->getMaxCapacity()->getValue(); if (($status === BookingStatus::APPROVED && $hasCapacity) || ($status === BookingStatus::PENDING && ($bookIfPending || (!$bookIfPending && $hasCapacity))) ) { $bookingStartTimes[] = $appointment->getBookingStart()->getValue()->format('H:i'); } } } return $bookingStartTimes; } /** @noinspection MoreThanThreeArgumentsInspection */ /** * @param Service $service * @param Collection $providers * @param array $globalDaysOffDates * @param \DateTime $startDateTime * @param \DateTime $endDateTime * @param int $personsCount * @param boolean $bookIfPending * @param boolean $bookIfNotMin * * @return array * @throws InvalidArgumentException * @throws \Exception */ public function getFreeTime( Service $service, Collection $providers, array $globalDaysOffDates, \DateTime $startDateTime, \DateTime $endDateTime, $personsCount, $bookIfPending, $bookIfNotMin ) { $weekDayIntervals = []; $appointmentIntervals = []; $providerDaysOffDates = []; $specialDayIntervals = []; foreach ((array)$providers->keys() as $providerKey) { $provider = $providers->getItem($providerKey); $appointmentIntervals[$providerKey] = $this->getProviderAppointmentIntervals($provider); $weekDayIntervals[$providerKey] = $this->getProviderOccupiedTimeIntervals($provider); $providerDaysOffDates[$providerKey] = $this->getProviderDayOffDates($provider); $specialDayIntervals[$providerKey] = $this->getProviderSpecialDayOccupiedTimeIntervals( $provider, $service->getId()->getValue() ); } $freeDateIntervals = []; foreach ($appointmentIntervals as $providerKey => $providerDates) { foreach ((array)$providerDates as $dateKey => $dateIntervals) { $dayIndex = DateTimeService::getDayIndex($dateKey); $specialDayDateKey = null; foreach ($specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) { if (in_array($dateKey, $specialDays['dates'])) { $specialDayDateKey = $specialDayKey; } } if ($specialDayDateKey !== null) { // get free intervals if it is special day $freeDateIntervals[$providerKey][$dateKey] = $this->getFreeIntervals( $this->mergeOverlappedIntervals( $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'] + $dateIntervals['intervals'] ), $specialDayIntervals[$providerKey][$specialDayDateKey]['startTime'], $specialDayIntervals[$providerKey][$specialDayDateKey]['endTime'] ); } else if (isset($weekDayIntervals[$providerKey][$dayIndex])) { // get free intervals if it is working day $freeDateIntervals[$providerKey][$dateKey] = $this->getFreeIntervals( $weekDayIntervals[$providerKey][$dayIndex]['intervals'] ? $this->mergeOverlappedIntervals( $weekDayIntervals[$providerKey][$dayIndex]['intervals'] + $dateIntervals['intervals'] ) : $dateIntervals['intervals'], $weekDayIntervals[$providerKey][$dayIndex]['startTime'], $weekDayIntervals[$providerKey][$dayIndex]['endTime'] ); } } } // create calendar $period = new \DatePeriod( $startDateTime, new \DateInterval('P1D'), $endDateTime ); $calendar = []; /** @var \DateTime $day */ foreach ($period as $day) { $currentDate = $day->format('Y-m-d'); $dayIndex = (int)$day->format('N'); $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) || array_key_exists($day->format('m-d'), $globalDaysOffDates); if (!$isGlobalDayOff) { foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) { $isProviderDayOff = array_key_exists($currentDate, $providerDaysOffDates[$providerKey]) || array_key_exists($day->format('m-d'), $providerDaysOffDates[$providerKey]); $specialDayDateKey = null; foreach ($specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) { if (in_array($currentDate, $specialDays['dates'])) { $specialDayDateKey = $specialDayKey; } } if (!$isProviderDayOff) { if ($freeDateIntervals && isset($freeDateIntervals[$providerKey]) && isset($freeDateIntervals[$providerKey][$currentDate]) ) { // get date intervals if there are appointments (special or working day) $calendar[$currentDate][$providerKey] = [ 'bookingStartTimes' => $personsCount && $bookIfNotMin ? $this->getProviderAppointmentBookingStartTimes( $service, $appointmentIntervals[$providerKey][$currentDate]['appointments'], $personsCount, $bookIfPending ) : [], 'intervals' => $freeDateIntervals[$providerKey][$currentDate], ]; } else { if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey])) { // get date intervals if it is special day with out appointments $calendar[$currentDate][$providerKey] = [ 'bookingStartTimes' => [], 'intervals' => $this->getFreeIntervals( $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'], $specialDayIntervals[$providerKey][$specialDayDateKey]['startTime'], $specialDayIntervals[$providerKey][$specialDayDateKey]['endTime'] ) ]; } else if (isset($weekDayIntervals[$providerKey][$dayIndex])) { // get date intervals if it is working day without appointments $calendar[$currentDate][$providerKey] = [ 'bookingStartTimes' => [], 'intervals' => $this->getFreeIntervals( $weekDayIntervals[$providerKey][$dayIndex]['intervals'], $weekDayIntervals[$providerKey][$dayIndex]['startTime'], $weekDayIntervals[$providerKey][$dayIndex]['endTime'] ) ]; } } } } } } return $calendar; } /** * @param WeekDay $weekDay * * @return array * @throws InvalidArgumentException */ public function getWeekDayIntervals($weekDay) { $startTime = $this->getSeconds( $weekDay->getStartTime()->getValue()->format('H:i:s') ); $endTime = $this->getSeconds( $weekDay->getEndTime()->getValue()->format('H:i:s') ); if ($endTime === 0) { $endTime = 24 * 60 * 60; } $intervals = []; foreach ((array)$weekDay->getTimeOutList()->keys() as $intervalKey) { $startIntervalTime = $this->getSeconds( $weekDay->getTimeOutList()->getItem($intervalKey)->getStartTime()->getValue()->format('H:i:s') ); $endIntervalTime = $this->getSeconds( $weekDay->getTimeOutList()->getItem($intervalKey)->getEndTime()->getValue()->format('H:i:s') ); if ($startTime <= $startIntervalTime && $endTime >= $endIntervalTime) { $intervals[$startIntervalTime] = [$startIntervalTime, $endIntervalTime]; } } return [ 'intervals' => $intervals, 'startTime' => $startTime, 'endTime' => $endTime, ]; } /** * @param array $appointments * * @return array */ public function getDateAppointmentsIntervals(array $appointments) { $intervals = []; foreach ($appointments as $appointment) { $startIntervalTime = $this->getSeconds($appointment->getBookingStart()->getValue()->format('H:i:s')); $endIntervalTime = $this->getSeconds( $appointment->getBookingEnd()->getValue()->format('H:i:s') === '00:00:00' ? '24:00:00' : $appointment->getBookingEnd()->getValue()->format('H:i:s') ); $timeBefore = 0; $timeAfter = 0; if ($appointment->getService()) { $timeBefore = $appointment->getService()->getTimeBefore() ? $appointment->getService()->getTimeBefore()->getValue() : 0; $timeAfter = $appointment->getService()->getTimeAfter() ? $appointment->getService()->getTimeAfter()->getValue() : 0; } // don't overwrite existing interval if it starts at the same time and last longer if (array_key_exists($startIntervalTime, $intervals) && $intervals[$startIntervalTime][1] > $endIntervalTime + $timeAfter) { continue; } $intervals[$startIntervalTime] = [ $startIntervalTime - $timeBefore, $endIntervalTime + $timeAfter ]; } $mergedIntervals = $this->mergeOverlappedIntervals($intervals); foreach ($mergedIntervals as $key => $interval) { if ($interval[0] !== $key) { $mergedIntervals[$interval[0]] = $interval; unset($mergedIntervals[$key]); } } return $mergedIntervals; } /** * @param string $time * * @return int */ public function getSeconds($time) { $timeParts = explode(':', $time); return $timeParts[0] * 60 * 60 + $timeParts[1] * 60 + $timeParts[2]; } /** * @param array $data * @param int $startTime * @param int $endTime * * @return array */ public function getFreeIntervals($data, $startTime, $endTime) { $result = []; ksort($data); $firstIntervalTime = true; $lastStartTime = $startTime; foreach ((array)$data as &$interval) { // Appointment is out of working hours if ($interval[0] >= $endTime || $interval[1] <= $startTime) { continue; } // Beginning or End of the Appointment is out of working hours if ($interval[0] < $startTime && $interval[1] <= $endTime) { $interval[0] = $startTime; } else if ($interval[0] >= $startTime && $interval[1] > $endTime) { $interval[1] = $endTime; } if ($lastStartTime !== $interval[0] && ($lastStartTime !== $startTime || ($firstIntervalTime && $lastStartTime !== $interval[0]))) { $firstIntervalTime = false; $result[$lastStartTime] = [ $lastStartTime, $interval[0] ]; } $lastStartTime = $interval[1]; } if ($lastStartTime !== $endTime) { $result[$lastStartTime] = [ $lastStartTime, $endTime ]; } return $result; } /** * @param array $data * * @return array */ public function mergeOverlappedIntervals($data) { // sort by key (time) ksort($data); // set first interval on top $result[] = array_shift($data); // add interval to the top OR replace top with merged intervals if they overlap foreach ((array)$data as $intervalValue) { $lastInterval = $result[count($result) - 1]; if ($lastInterval[1] < $intervalValue[0]) { $result[] = $intervalValue; } else if ($lastInterval[1] < $intervalValue[1]) { $lastInterval[1] = $intervalValue[1]; array_pop($result); $result[] = $lastInterval; } } return $result; } /** @noinspection MoreThanThreeArgumentsInspection */ /** * @param Service $service * @param int $requiredTime * @param array $freeIntervals * @param int $slotLength * @param \DateTime $startDateTime * @param bool $serviceDurationAsSlot * * @return array */ public function getAppointmentFreeSlots( $service, $requiredTime, $freeIntervals, $slotLength, $startDateTime, $serviceDurationAsSlot ) { $result = []; $currentTimeInSeconds = $this->getSeconds(DateTimeService::getNowDateTimeObject()->format('H:i:s')); $startTimeInSeconds = $this->getSeconds($startDateTime->format('H:i:s')); $startDateFormatted = $startDateTime->format('Y-m-d'); foreach ($freeIntervals as $dateKey => $dateProviders) { foreach ((array)$dateProviders as $providerKey => $provider) { foreach ((array)$provider['intervals'] as $timePeriod) { $customerTimeStart = $timePeriod[0] + $service->getTimeBefore()->getValue(); $remainTime = $customerTimeStart % 3600; if ($remainTime !== 0) { $remainTimeSlots = floor((3600 - $remainTime) / $slotLength); $customerTimeStart = $customerTimeStart - $remainTime - ($remainTimeSlots * $slotLength) + 3600; } $bookingLength = $serviceDurationAsSlot ? $requiredTime : $slotLength; $providerTimeStart = $customerTimeStart - $service->getTimeBefore()->getValue(); $numberOfSlots = floor(($timePeriod[1] - $providerTimeStart - $requiredTime) / $bookingLength) + 1; for ($i = 0; $i < $numberOfSlots; $i++) { $timeSlot = $customerTimeStart + $i * $bookingLength; $time = sprintf('%02d', floor($timeSlot / 3600)) . ':' . sprintf('%02d', floor(($timeSlot / 60) % 60)); if ($startDateFormatted !== $dateKey || ($startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot && $currentTimeInSeconds < $timeSlot)) { $result[$dateKey][$time][] = $providerKey; } } } foreach ((array)$provider['bookingStartTimes'] as $appointmentInterval) { $time = $appointmentInterval; $result[$dateKey][$time][] = $providerKey; } } if (isset($result[$dateKey])) { if (!$result[$dateKey]) { unset($result[$dateKey]); } else { ksort($result[$dateKey]); } } } return $result; } }