19 private const OPTION_NAME =
'isOccupancyCheckerEnabled';
20 private const OPTION_ENABLED =
'Y';
21 private const OPTION_DISABLED =
'N';
23 private const CHECK_LIMIT = 2 * 365 * 86400;
24 private const DISTURBING_EVENTS_LIMIT = 3;
25 private const KEY_PART_START =
'start';
26 private const KEY_PART_END =
'end';
28 private array $timeline = [];
29 private int $checkEventId = 0;
30 private array $cachedEvents = [];
47 if (!$this->canCheck(
$event))
64 $this->fillTimeline(
$event, $eventsForCheck, $existingEvents);
65 asort($this->timeline, SORT_NUMERIC);
67 $checkingEventsStartedCount = 0;
68 $existingEventsStartedCount = 0;
69 $disturbingEventDates = [];
70 $isDisturbingEventsAmountOverShowLimit =
false;
71 foreach ($this->timeline as
$key => $value)
73 [$eventId, $repeatNumber, $isEnd] = $this->parseTimelineKey(
$key);
74 if ($eventId === $this->checkEventId)
78 $checkingEventsStartedCount--;
82 $checkingEventsStartedCount++;
87 $existingEventsStartedCount--;
91 $existingEventsStartedCount++;
94 if ($checkingEventsStartedCount > 0 && $existingEventsStartedCount > 0)
96 $disturbingEvent = $this->cachedEvents[$this->getCachedEventKey($eventId, $repeatNumber)];;
97 if (
count($disturbingEventDates) >= self::DISTURBING_EVENTS_LIMIT)
99 $isDisturbingEventsAmountOverShowLimit =
true;
102 $disturbingEventDate = $this->getEventFormattedDate($disturbingEvent);
103 if (!in_array($disturbingEventDate, $disturbingEventDates,
true))
105 $disturbingEventDates[] = $disturbingEventDate;
110 if (!empty($disturbingEventDates))
114 'disturbingEventsFormatted' => implode(
', ', $disturbingEventDates),
115 'isDisturbingEventsAmountOverShowLimit' => $isDisturbingEventsAmountOverShowLimit,
130 && !empty(
$event[
'LOCATION'][
'NEW'])
131 && !empty(
$event[
'DATE_FROM_TS_UTC'])
132 && !empty(
$event[
'DATE_TO_TS_UTC'])
133 && !empty(
$event[
'RRULE'])
134 && !empty(
$event[
'DATE_FROM'])
135 && !empty(
$event[
'DATE_TO'])
142 private function isEnabled(): bool
144 return Option::get(
'calendar', self::OPTION_NAME, self::OPTION_ENABLED,
'-') !== self::OPTION_DISABLED;
154 $toLimit = $this->getCheckLimit(
$event);
156 \CCalendarEvent::ParseRecursion(
160 'userId' => \CCalendar::GetCurUserId(),
162 'toLimitTs' => $toLimit,
164 'instanceCount' =>
false,
165 'preciseLimits' =>
false,
166 'checkPermission' =>
false,
170 return $checkedEvents;
179 return min(
$event[
'DATE_FROM_TS_UTC'] + self::CHECK_LIMIT,
$event[
'DATE_TO_TS_UTC']);
191 $timezoneFrom =
$event[
'TZ_FROM'] ??
null;
192 $timezoneTo =
$event[
'TZ_TO'] ??
null;
195 if (($timestampFrom === $timestampTo) || ((
$event[
'DT_SKIP_TIME'] ??
null) ===
'Y'))
197 $timestampTo += \CCalendar::GetDayLen();
204 return [$timestampFrom, $timestampTo];
239 $toLimit = $this->getCheckLimit(
$event);
241 $sections = [$roomId];
244 if (!empty($additionalLocationConnection))
248 ...$additionalLocationConnection,
252 return \CCalendarEvent::GetList(
254 'arSelect' => $arSelect,
256 'SECTION' => $sections,
257 'FROM_LIMIT' =>
$event[
'DATE_FROM'],
258 'TO_LIMIT' => DateTime::createFromTimestamp($toLimit)->toString(),
262 'parseRecursion' =>
true,
263 'fetchAttendees' =>
false,
264 'fetchMeetings' =>
false,
265 'setDefaultLimit' =>
false,
267 'checkPermissions' =>
false,
268 'parseDescription' =>
false,
269 'fetchSection' =>
false,
270 'getUserfields' =>
false,
285 $this->checkEventId =
$event[
'ID'];
289 $previousEventId = 0;
290 foreach ($existingEvents as $existingEvent)
292 if (!$this->canFindIntersections(
$event, $existingEvent))
297 $currentEventId = (int)$existingEvent[
'PARENT_ID'];
298 if ($currentEventId !== $previousEventId)
300 $previousEventId = $currentEventId;
306 [$timestampFrom, $timestampTo] = $this->getEventTimestamps($existingEvent);
308 $this->timeline[$this->getTimelineKey($currentEventId, $currentIndex,
false)] = $timestampFrom;
309 $this->timeline[$this->getTimelineKey($currentEventId, $currentIndex,
true)] = $timestampTo;
310 $this->cachedEvents[$this->getCachedEventKey($currentEventId, $currentIndex)] = [
311 'ID' => $currentEventId,
312 'DATE_FROM' => $existingEvent[
'DATE_FROM'],
313 'TZ_FROM' => $existingEvent[
'TZ_FROM'],
317 catch (ObjectException $exception)
323 foreach ($eventsForCheck as $eventForCheck)
325 if (empty($eventForCheck[
'DATE_FROM']) || empty($eventForCheck[
'DATE_TO']))
329 [$timestampFrom, $timestampTo] = $this->getEventTimestamps($eventForCheck);
331 $this->timeline[$this->getTimelineKey($this->checkEventId, $currentIndex,
false)] = $timestampFrom;
332 $this->timeline[$this->getTimelineKey($this->checkEventId, $currentIndex,
true)] = $timestampTo;
333 $this->cachedEvents[$this->getCachedEventKey($eventForCheck[
'ID'], $currentIndex)] = [
334 'ID' => $eventForCheck[
'ID'],
335 'DATE_FROM' => $eventForCheck[
'DATE_FROM'],
336 'TZ_FROM' => $eventForCheck[
'TZ_FROM'],
347 private function canFindIntersections(
array $event,
array $existingEvent): bool
350 (
$event[
'PARENT_ID'] ??
null) !== (int)($existingEvent[
'PARENT_ID'] ??
null)
351 && !empty($existingEvent[
'PARENT_ID'])
352 && !empty($existingEvent[
'DATE_FROM'])
353 && !empty($existingEvent[
'DATE_TO'])
354 && !empty($existingEvent[
'ID'])
364 private function getTimelineKey(
int $eventId,
int $repeatNumber,
bool $isEnd): string
366 return $eventId .
'_' . $repeatNumber .
'_' . ($isEnd ? self::KEY_PART_END : self::KEY_PART_START);
373 private function parseTimelineKey(
string $key):
array
376 return [(int)
$res[0], (
int)
$res[1], (
$res[2] === self::KEY_PART_END)];
384 private function getCachedEventKey(
int $eventId,
int $repeatNumber): string
386 return $eventId .
'_' . $repeatNumber;
393 private function getEventFormattedDate(
array $event): string
395 $eventTimezone =
null;
396 if (!empty(
$event[
'TZ_FROM']))
398 $eventTimezone = new \DateTimeZone(
$event[
'TZ_FROM']);
403 $result = \Bitrix\Calendar\Util::formatEventDate(
404 new DateTime(
$event[
'DATE_FROM'],
null, $eventTimezone)
407 catch (ObjectException $e)